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