1 # SPDX-License-Identifier: GPL-2.0-only
 
   3 # This file is part of Nominatim. (https://nominatim.org)
 
   5 # Copyright (C) 2022 by the Nominatim developer community.
 
   6 # For a full list of authors see the git log.
 
   8 Helper functions for executing external programs.
 
  10 from typing import Any, Union, Optional, Mapping, IO
 
  11 from pathlib import Path
 
  15 import urllib.request as urlrequest
 
  16 from urllib.parse import urlencode
 
  18 from nominatim.typing import StrPath
 
  19 from nominatim.version import version_str
 
  20 from nominatim.db.connection import get_pg_env
 
  22 LOG = logging.getLogger()
 
  24 def run_legacy_script(script: StrPath, *args: Union[int, str],
 
  26                       throw_on_fail: bool = False) -> int:
 
  27     """ Run a Nominatim PHP script with the given arguments.
 
  29         Returns the exit code of the script. If `throw_on_fail` is True
 
  30         then throw a `CalledProcessError` on a non-zero exit.
 
  32     cmd = ['/usr/bin/env', 'php', '-Cq',
 
  33            str(nominatim_env.phplib_dir / 'admin' / script)]
 
  34     cmd.extend([str(a) for a in args])
 
  36     env = nominatim_env.config.get_os_env()
 
  37     env['NOMINATIM_DATADIR'] = str(nominatim_env.data_dir)
 
  38     env['NOMINATIM_SQLDIR'] = str(nominatim_env.sqllib_dir)
 
  39     env['NOMINATIM_CONFIGDIR'] = str(nominatim_env.config_dir)
 
  40     env['NOMINATIM_DATABASE_MODULE_SRC_PATH'] = str(nominatim_env.module_dir)
 
  41     if not env['NOMINATIM_OSM2PGSQL_BINARY']:
 
  42         env['NOMINATIM_OSM2PGSQL_BINARY'] = str(nominatim_env.osm2pgsql_path)
 
  44     proc = subprocess.run(cmd, cwd=str(nominatim_env.project_dir), env=env,
 
  47     return proc.returncode
 
  49 def run_api_script(endpoint: str, project_dir: Path,
 
  50                    extra_env: Optional[Mapping[str, str]] = None,
 
  51                    phpcgi_bin: Optional[Path] = None,
 
  52                    params: Optional[Mapping[str, Any]] = None) -> int:
 
  53     """ Execute a Nominatim API function.
 
  55         The function needs a project directory that contains the website
 
  56         directory with the scripts to be executed. The scripts will be run
 
  57         using php_cgi. Query parameters can be added as named arguments.
 
  59         Returns the exit code of the script.
 
  61     log = logging.getLogger()
 
  62     webdir = str(project_dir / 'website')
 
  63     query_string = urlencode(params or {})
 
  65     env = dict(QUERY_STRING=query_string,
 
  66                SCRIPT_NAME=f'/{endpoint}.php',
 
  67                REQUEST_URI=f'/{endpoint}.php?{query_string}',
 
  68                CONTEXT_DOCUMENT_ROOT=webdir,
 
  69                SCRIPT_FILENAME=f'{webdir}/{endpoint}.php',
 
  70                HTTP_HOST='localhost',
 
  71                HTTP_USER_AGENT='nominatim-tool',
 
  72                REMOTE_ADDR='0.0.0.0',
 
  75                SERVER_PROTOCOL='HTTP/1.1',
 
  76                GATEWAY_INTERFACE='CGI/1.1',
 
  77                REDIRECT_STATUS='CGI')
 
  82     if phpcgi_bin is None:
 
  83         cmd = ['/usr/bin/env', 'php-cgi']
 
  85         cmd = [str(phpcgi_bin)]
 
  87     proc = subprocess.run(cmd, cwd=str(project_dir), env=env,
 
  88                           stdout=subprocess.PIPE,
 
  89                           stderr=subprocess.PIPE,
 
  92     if proc.returncode != 0 or proc.stderr:
 
  94             log.error(proc.stderr.decode('utf-8').replace('\\n', '\n'))
 
  96             log.error(proc.stdout.decode('utf-8').replace('\\n', '\n'))
 
  97         return proc.returncode or 1
 
  99     result = proc.stdout.decode('utf-8')
 
 100     content_start = result.find('\r\n\r\n')
 
 102     print(result[content_start + 4:].replace('\\n', '\n'))
 
 107 def run_php_server(server_address: str, base_dir: StrPath) -> None:
 
 108     """ Run the built-in server from the given directory.
 
 110     subprocess.run(['/usr/bin/env', 'php', '-S', server_address],
 
 111                    cwd=str(base_dir), check=True)
 
 114 def run_osm2pgsql(options: Mapping[str, Any]) -> None:
 
 115     """ Run osm2pgsql with the given options.
 
 117     env = get_pg_env(options['dsn'])
 
 118     cmd = [str(options['osm2pgsql']),
 
 119            '--hstore', '--latlon', '--slim',
 
 120            '--with-forward-dependencies', 'false',
 
 121            '--log-progress', 'true',
 
 122            '--number-processes', str(options['threads']),
 
 123            '--cache', str(options['osm2pgsql_cache']),
 
 124            '--style', str(options['osm2pgsql_style'])
 
 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'))
 
 132         cmd.extend(('--output', 'gazetteer'))
 
 134     if options['append']:
 
 135         cmd.append('--append')
 
 137         cmd.append('--create')
 
 139     if options['flatnode_file']:
 
 140         cmd.extend(('--flat-nodes', options['flatnode_file']))
 
 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]))
 
 149     if options.get('disable_jit', False):
 
 150         env['PGOPTIONS'] = '-c jit=off -c max_parallel_workers_per_gather=0'
 
 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))
 
 158         cmd.append(str(options['import_file']))
 
 160     subprocess.run(cmd, cwd=options.get('cwd', '.'),
 
 161                    input=options.get('import_data'),
 
 165 def get_url(url: str) -> str:
 
 166     """ Get the contents from the given URL and return it as a UTF-8 string.
 
 168     headers = {"User-Agent": f"Nominatim/{version_str()}"}
 
 171         request = urlrequest.Request(url, headers=headers)
 
 172         with urlrequest.urlopen(request) as response: # type: IO[bytes]
 
 173             return response.read().decode('utf-8')
 
 175         LOG.fatal('Failed to load URL: %s', url)