Files
aurora-dockside/src/main/native/nativeProject.ts
T

294 lines
11 KiB
TypeScript

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 `<?php
// Generated by Aurora Core. This endpoint binds only to the project's loopback server.
?>
<script>
addEventListener('DOMContentLoaded', () => {
const username = document.querySelector('input[name="auth[username]"]');
if (!username) return;
const form = username.form;
form.querySelector('input[name="auth[server]"]').value = '127.0.0.1:${project.ports.database}';
username.value = 'db';
form.querySelector('input[name="auth[password]"]').value = 'db';
form.querySelector('input[name="auth[db]"]').value = 'db';
const permanent = form.querySelector('input[name="auth[permanent]"]');
if (permanent) permanent.checked = true;
form.submit();
});
</script>
<?php
require '${adminer.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}';
`
}
export function renderMariaDbConfig(
project: NativeProjectDefinition,
installedRuntimeRoot = runtimeRoot()
): string {
const directory = nativeDirectory(project.root)
const basedir =
process.platform === 'win32'
? join(installedRuntimeRoot, 'bin', 'mariadb')
: join(installedRuntimeRoot, 'root', 'usr')
const configPath = (value: string): string => 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<void> {
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<NativeServiceSpec, 'id' | 'cwd' | 'logPath' | 'pidPath'> => ({
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<void> {
await provisionNativeProject(project)
for (const spec of nativeServiceSpecs(project)) await supervisor.start(spec)
}
export async function stopNativeProject(project: NativeProjectDefinition): Promise<void> {
for (const spec of nativeServiceSpecs(project).reverse()) await supervisor.stop(spec)
}
export async function nativeProjectStatus(
project: NativeProjectDefinition
): Promise<{ running: boolean; services: Record<string, 'running' | 'stopped'> }> {
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
}
}