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 nativeDirectory(root: string): string { return join(root, '.aurora', 'native') } function quoteNginx(value: string): string { return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"') } 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 = join(installedRoot(project), 'root', 'usr', 'share', 'aurora', 'adminer.php') return ` { 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)) ) ]) try { await access(join(directory, 'data', 'mariadb', 'mysql')) } catch { await execFileAsync( join(installedRoot(project), 'bin', 'mariadb-install-db'), [ '--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: join(installedRoot(project), 'bin', 'mariadbd'), args: [`--defaults-file=${join(directory, 'config', 'mariadb.cnf')}`], ready: { port: project.ports.database, timeoutMs: 30000 } }, { ...common('php'), command: join(installedRoot(project), 'bin', 'php-fpm'), args: ['--nodaemonize', '--fpm-config', join(directory, 'config', 'php-fpm.conf')], ready: { port: project.ports.php } }, { ...common('web'), command: join(installedRoot(project), 'bin', 'nginx'), args: ['-c', join(directory, 'config', 'nginx.conf'), '-p', `${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 } }