import { execFile } from 'child_process' import { access, mkdir, writeFile } from 'fs/promises' import { join, resolve } from 'path' import { promisify } from 'util' import type { AuroraNativePorts } from './portAllocator' import { NativeProcessSupervisor, type NativeServiceSpec } from './processSupervisor' import { runtimeRoot } from '../nativeRuntime' const execFileAsync = promisify(execFile) const supervisor = new NativeProcessSupervisor() export interface NativeProjectDefinition { name: string root: string docroot: string ports: AuroraNativePorts installedRuntimeRoot?: string } function installedRoot(project: NativeProjectDefinition): string { return project.installedRuntimeRoot ?? runtimeRoot() } function runtimeExecutable(project: NativeProjectDefinition, name: string): string { if (process.platform !== 'win32') return join(installedRoot(project), 'bin', name) if (name === 'php' || name === 'php-cgi') return join(installedRoot(project), 'bin', 'php', `${name}.exe`) if (name === 'nginx') return join(installedRoot(project), 'bin', 'nginx', 'nginx.exe') return join(installedRoot(project), 'bin', 'mariadb', 'bin', `${name}.exe`) } function nativeDirectory(root: string): string { return join(root, '.aurora', 'native') } function quoteNginx(value: string): string { return nativeConfigPath(value).replace(/"/g, '\\"') } export function nativeConfigPath(value: string, platform = process.platform): string { return platform === 'win32' ? value.replace(/\\/g, '/') : value } export function renderPhpFpmConfig(project: NativeProjectDefinition): string { const directory = nativeDirectory(project.root) return `[global] daemonize = no pid = ${join(directory, 'pids', 'php.pid')} error_log = ${join(directory, 'logs', 'php.log')} [www] listen = 127.0.0.1:${project.ports.php} pm = dynamic pm.max_children = 8 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 3 clear_env = no catch_workers_output = yes chdir = ${project.root} ` } export function renderNginxConfig(project: NativeProjectDefinition): string { const directory = nativeDirectory(project.root) const webroot = resolve(project.root, project.docroot || '.') return `daemon off; pid "${quoteNginx(join(directory, 'pids', 'nginx.pid'))}"; error_log "${quoteNginx(join(directory, 'logs', 'nginx.log'))}" info; events { worker_connections 256; } http { access_log "${quoteNginx(join(directory, 'logs', 'nginx-access.log'))}"; client_body_temp_path "${quoteNginx(join(directory, 'tmp', 'nginx-client'))}"; proxy_temp_path "${quoteNginx(join(directory, 'tmp', 'nginx-proxy'))}"; fastcgi_temp_path "${quoteNginx(join(directory, 'tmp', 'nginx-fastcgi'))}"; uwsgi_temp_path "${quoteNginx(join(directory, 'tmp', 'nginx-uwsgi'))}"; scgi_temp_path "${quoteNginx(join(directory, 'tmp', 'nginx-scgi'))}"; server { listen 127.0.0.1:${project.ports.http}; server_name localhost; root "${quoteNginx(webroot)}"; index index.php index.html; location = /__aurora/adminer/ { fastcgi_pass 127.0.0.1:${project.ports.php}; fastcgi_param SCRIPT_FILENAME "${quoteNginx(join(directory, 'adminer.php'))}"; fastcgi_param SCRIPT_NAME /__aurora/adminer/; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param REQUEST_URI $request_uri; fastcgi_param QUERY_STRING $query_string; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param SERVER_NAME $server_name; fastcgi_param SERVER_PORT $server_port; fastcgi_param HTTP_HOST $http_host; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param REMOTE_PORT $remote_port; } location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \\.php$ { fastcgi_pass 127.0.0.1:${project.ports.php}; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param REQUEST_URI $request_uri; fastcgi_param QUERY_STRING $query_string; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param DOCUMENT_ROOT $document_root; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param SERVER_NAME $server_name; fastcgi_param SERVER_PORT $server_port; fastcgi_param HTTP_HOST $http_host; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param REMOTE_PORT $remote_port; } } } ` } export function renderNativeAdminerBootstrap(project: NativeProjectDefinition): string { const adminer = process.platform === 'win32' ? join(installedRoot(project), 'tools', 'adminer.php') : join(installedRoot(project), 'root', 'usr', 'share', 'aurora', 'adminer.php') return ` nativeConfigPath(value) return `[mariadbd] basedir="${configPath(basedir)}" datadir="${configPath(join(directory, 'data', 'mariadb'))}" tmpdir="${configPath(join(directory, 'tmp'))}" bind-address=127.0.0.1 port=${project.ports.database} ${process.platform === 'win32' ? '' : `socket=${join(directory, 'mariadb.sock')}\n`}pid-file="${configPath(join(directory, 'pids', 'mariadb.pid'))}" log-error="${configPath(join(directory, 'logs', 'mariadb.log'))}" skip-name-resolve ` } export async function provisionNativeProject(project: NativeProjectDefinition): Promise { const directory = nativeDirectory(project.root) for (const child of [ 'config', 'data/mariadb', 'logs', 'pids', 'tmp', 'tmp/nginx-client', 'tmp/nginx-proxy', 'tmp/nginx-fastcgi', 'tmp/nginx-uwsgi', 'tmp/nginx-scgi' ]) await mkdir(join(directory, child), { recursive: true }) await Promise.all([ writeFile(join(directory, 'config', 'php-fpm.conf'), renderPhpFpmConfig(project)), writeFile(join(directory, 'config', 'nginx.conf'), renderNginxConfig(project)), writeFile(join(directory, 'adminer.php'), renderNativeAdminerBootstrap(project)), writeFile( join(directory, 'config', 'mariadb.cnf'), renderMariaDbConfig(project, installedRoot(project)) ), ...(process.platform === 'win32' ? [ writeFile( join(directory, 'config', 'php.ini'), `extension_dir="${nativeConfigPath(join(installedRoot(project), 'bin', 'php', 'ext'))}"\nextension=mysqli\nextension=pdo_mysql\nextension=mbstring\nextension=curl\nextension=openssl\nextension=zip\ndisplay_errors=On\nlog_errors=On\nerror_log="${nativeConfigPath(join(directory, 'logs', 'php.log'))}"\n` ) ] : []) ]) try { await access(join(directory, 'data', 'mariadb', 'mysql')) } catch { await execFileAsync( runtimeExecutable(project, 'mariadb-install-db'), process.platform === 'win32' ? [`--datadir=${join(directory, 'data', 'mariadb')}`, '--password='] : [ '--no-defaults', `--datadir=${join(directory, 'data', 'mariadb')}`, `--tmpdir=${join(directory, 'tmp')}`, '--auth-root-authentication-method=normal', '--skip-test-db' ], { cwd: project.root, maxBuffer: 16 * 1024 * 1024 } ) } } export function nativeServiceSpecs(project: NativeProjectDefinition): NativeServiceSpec[] { const directory = nativeDirectory(project.root) const common = (id: string): Pick => ({ id: `${project.name}:${id}`, cwd: project.root, logPath: join(directory, 'logs', `${id}-process.log`), pidPath: join(directory, 'pids', `${id}-process.json`) }) return [ { ...common('database'), command: runtimeExecutable(project, 'mariadbd'), args: [ `--defaults-file=${nativeConfigPath(join(directory, 'config', 'mariadb.cnf'))}`, ...(process.platform === 'win32' ? ['--console'] : []) ], ready: { port: project.ports.database, timeoutMs: 30000 } }, { ...common('php'), command: runtimeExecutable(project, process.platform === 'win32' ? 'php-cgi' : 'php-fpm'), args: process.platform === 'win32' ? ['-b', `127.0.0.1:${project.ports.php}`] : ['--nodaemonize', '--fpm-config', join(directory, 'config', 'php-fpm.conf')], env: process.platform === 'win32' ? { PHPRC: join(directory, 'config', 'php.ini'), PHP_FCGI_MAX_REQUESTS: '0' } : undefined, ready: { port: project.ports.php } }, { ...common('web'), command: runtimeExecutable(project, 'nginx'), args: [ '-c', nativeConfigPath(join(directory, 'config', 'nginx.conf')), '-p', `${nativeConfigPath(directory)}/` ], ready: { port: project.ports.http } } ] } export async function startNativeProject(project: NativeProjectDefinition): Promise { await provisionNativeProject(project) for (const spec of nativeServiceSpecs(project)) await supervisor.start(spec) } export async function stopNativeProject(project: NativeProjectDefinition): Promise { for (const spec of nativeServiceSpecs(project).reverse()) await supervisor.stop(spec) } export async function nativeProjectStatus( project: NativeProjectDefinition ): Promise<{ running: boolean; services: Record }> { const entries = await Promise.all( nativeServiceSpecs(project).map( async (spec) => [spec.id.split(':').at(-1)!, await supervisor.status(spec)] as const ) ) const services = Object.fromEntries(entries) return { running: entries.length > 0 && entries.every(([, status]) => status === 'running'), services } }