]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/tools/exec_utils.py
Merge remote-tracking branch 'upstream/master'
[nominatim.git] / nominatim / tools / exec_utils.py
1 # SPDX-License-Identifier: GPL-2.0-only
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2022 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Helper functions for executing external programs.
9 """
10 from typing import Any, Union, Optional, Mapping, IO
11 from pathlib import Path
12 import logging
13 import subprocess
14 import urllib.request as urlrequest
15 from urllib.parse import urlencode
16
17 from nominatim.typing import StrPath
18 from nominatim.version import version_str
19 from nominatim.db.connection import get_pg_env
20
21 LOG = logging.getLogger()
22
23 def run_legacy_script(script: StrPath, *args: Union[int, str],
24                       nominatim_env: Any,
25                       throw_on_fail: bool = False) -> int:
26     """ Run a Nominatim PHP script with the given arguments.
27
28         Returns the exit code of the script. If `throw_on_fail` is True
29         then throw a `CalledProcessError` on a non-zero exit.
30     """
31     cmd = ['/usr/bin/env', 'php', '-Cq',
32            str(nominatim_env.phplib_dir / 'admin' / script)]
33     cmd.extend([str(a) for a in args])
34
35     env = nominatim_env.config.get_os_env()
36     env['NOMINATIM_DATADIR'] = str(nominatim_env.data_dir)
37     env['NOMINATIM_SQLDIR'] = str(nominatim_env.sqllib_dir)
38     env['NOMINATIM_CONFIGDIR'] = str(nominatim_env.config_dir)
39     env['NOMINATIM_DATABASE_MODULE_SRC_PATH'] = str(nominatim_env.module_dir)
40     if not env['NOMINATIM_OSM2PGSQL_BINARY']:
41         env['NOMINATIM_OSM2PGSQL_BINARY'] = str(nominatim_env.osm2pgsql_path)
42
43     proc = subprocess.run(cmd, cwd=str(nominatim_env.project_dir), env=env,
44                           check=throw_on_fail)
45
46     return proc.returncode
47
48 def run_api_script(endpoint: str, project_dir: Path,
49                    extra_env: Optional[Mapping[str, str]] = None,
50                    phpcgi_bin: Optional[Path] = None,
51                    params: Optional[Mapping[str, Any]] = None) -> int:
52     """ Execute a Nominatim API function.
53
54         The function needs a project directory that contains the website
55         directory with the scripts to be executed. The scripts will be run
56         using php_cgi. Query parameters can be added as named arguments.
57
58         Returns the exit code of the script.
59     """
60     log = logging.getLogger()
61     webdir = str(project_dir / 'website')
62     query_string = urlencode(params or {})
63
64     env = dict(QUERY_STRING=query_string,
65                SCRIPT_NAME=f'/{endpoint}.php',
66                REQUEST_URI=f'/{endpoint}.php?{query_string}',
67                CONTEXT_DOCUMENT_ROOT=webdir,
68                SCRIPT_FILENAME=f'{webdir}/{endpoint}.php',
69                HTTP_HOST='localhost',
70                HTTP_USER_AGENT='nominatim-tool',
71                REMOTE_ADDR='0.0.0.0',
72                DOCUMENT_ROOT=webdir,
73                REQUEST_METHOD='GET',
74                SERVER_PROTOCOL='HTTP/1.1',
75                GATEWAY_INTERFACE='CGI/1.1',
76                REDIRECT_STATUS='CGI')
77
78     if extra_env:
79         env.update(extra_env)
80
81     if phpcgi_bin is None:
82         cmd = ['/usr/bin/env', 'php-cgi']
83     else:
84         cmd = [str(phpcgi_bin)]
85
86     proc = subprocess.run(cmd, cwd=str(project_dir), env=env,
87                           stdout=subprocess.PIPE,
88                           stderr=subprocess.PIPE,
89                           check=False)
90
91     if proc.returncode != 0 or proc.stderr:
92         if proc.stderr:
93             log.error(proc.stderr.decode('utf-8').replace('\\n', '\n'))
94         else:
95             log.error(proc.stdout.decode('utf-8').replace('\\n', '\n'))
96         return proc.returncode or 1
97
98     result = proc.stdout.decode('utf-8')
99     content_start = result.find('\r\n\r\n')
100
101     print(result[content_start + 4:].replace('\\n', '\n'))
102
103     return 0
104
105
106 def run_php_server(server_address: str, base_dir: StrPath) -> None:
107     """ Run the built-in server from the given directory.
108     """
109     subprocess.run(['/usr/bin/env', 'php', '-S', server_address],
110                    cwd=str(base_dir), check=True)
111
112
113 def run_osm2pgsql(options: Mapping[str, Any]) -> None:
114     """ Run osm2pgsql with the given options.
115     """
116     env = get_pg_env(options['dsn'])
117     cmd = [str(options['osm2pgsql']),
118            '--hstore', '--latlon', '--slim',
119            '--with-forward-dependencies', 'false',
120            '--log-progress', 'true',
121            '--number-processes', str(options['threads']),
122            '--cache', str(options['osm2pgsql_cache']),
123            '--output', 'gazetteer',
124            '--style', str(options['osm2pgsql_style'])
125           ]
126     if options['append']:
127         cmd.append('--append')
128     else:
129         cmd.append('--create')
130
131     if options['flatnode_file']:
132         cmd.extend(('--flat-nodes', options['flatnode_file']))
133
134     for key, param in (('slim_data', '--tablespace-slim-data'),
135                        ('slim_index', '--tablespace-slim-index'),
136                        ('main_data', '--tablespace-main-data'),
137                        ('main_index', '--tablespace-main-index')):
138         if options['tablespaces'][key]:
139             cmd.extend((param, options['tablespaces'][key]))
140
141     if options.get('disable_jit', False):
142         env['PGOPTIONS'] = '-c jit=off -c max_parallel_workers_per_gather=0'
143
144     if 'import_data' in options:
145         cmd.extend(('-r', 'xml', '-'))
146     elif isinstance(options['import_file'], list):
147         for fname in options['import_file']:
148             cmd.append(str(fname))
149     else:
150         cmd.append(str(options['import_file']))
151
152     subprocess.run(cmd, cwd=options.get('cwd', '.'),
153                    input=options.get('import_data'),
154                    env=env, check=True)
155
156
157 def get_url(url: str) -> str:
158     """ Get the contents from the given URL and return it as a UTF-8 string.
159     """
160     headers = {"User-Agent": f"Nominatim/{version_str()}"}
161
162     try:
163         request = urlrequest.Request(url, headers=headers)
164         with urlrequest.urlopen(request) as response: # type: IO[bytes]
165             return response.read().decode('utf-8')
166     except Exception:
167         LOG.fatal('Failed to load URL: %s', url)
168         raise