2026-08-22 03:47:09 -05:00
|
|
|
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
|
2026-08-22 04:11:44 -05:00
|
|
|
installedRuntimeRoot?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function installedRoot(project: NativeProjectDefinition): string {
|
|
|
|
|
return project.installedRuntimeRoot ?? runtimeRoot()
|
2026-08-22 03:47:09 -05:00
|
|
|
}
|
|
|
|
|
|
2026-08-26 20:54:58 -05:00
|
|
|
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`)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-22 03:47:09 -05:00
|
|
|
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'))}";
|
2026-08-22 04:11:44 -05:00
|
|
|
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'))}";
|
2026-08-22 03:47:09 -05:00
|
|
|
server {
|
|
|
|
|
listen 127.0.0.1:${project.ports.http};
|
|
|
|
|
server_name localhost;
|
|
|
|
|
root "${quoteNginx(webroot)}";
|
|
|
|
|
index index.php index.html;
|
2026-08-22 04:26:37 -05:00
|
|
|
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;
|
|
|
|
|
}
|
2026-08-22 03:47:09 -05:00
|
|
|
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;
|
2026-08-22 04:11:44 -05:00
|
|
|
fastcgi_param REQUEST_URI $request_uri;
|
2026-08-22 03:47:09 -05:00
|
|
|
fastcgi_param QUERY_STRING $query_string;
|
|
|
|
|
fastcgi_param CONTENT_TYPE $content_type;
|
|
|
|
|
fastcgi_param CONTENT_LENGTH $content_length;
|
2026-08-22 04:11:44 -05:00
|
|
|
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;
|
2026-08-22 03:47:09 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-22 04:26:37 -05:00
|
|
|
export function renderNativeAdminerBootstrap(project: NativeProjectDefinition): string {
|
2026-08-26 20:54:58 -05:00
|
|
|
const adminer =
|
|
|
|
|
process.platform === 'win32'
|
|
|
|
|
? join(installedRoot(project), 'tools', 'adminer.php')
|
|
|
|
|
: join(installedRoot(project), 'root', 'usr', 'share', 'aurora', 'adminer.php')
|
2026-08-22 04:26:37 -05:00
|
|
|
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, "\\'")}';
|
|
|
|
|
`
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-22 03:47:09 -05:00
|
|
|
export function renderMariaDbConfig(
|
|
|
|
|
project: NativeProjectDefinition,
|
|
|
|
|
installedRuntimeRoot = runtimeRoot()
|
|
|
|
|
): string {
|
|
|
|
|
const directory = nativeDirectory(project.root)
|
2026-08-26 20:54:58 -05:00
|
|
|
const basedir =
|
|
|
|
|
process.platform === 'win32'
|
|
|
|
|
? join(installedRuntimeRoot, 'bin', 'mariadb')
|
|
|
|
|
: join(installedRuntimeRoot, 'root', 'usr')
|
2026-08-22 03:47:09 -05:00
|
|
|
return `[mariadbd]
|
2026-08-26 20:54:58 -05:00
|
|
|
basedir=${basedir}
|
2026-08-22 03:47:09 -05:00
|
|
|
datadir=${join(directory, 'data', 'mariadb')}
|
|
|
|
|
tmpdir=${join(directory, 'tmp')}
|
|
|
|
|
bind-address=127.0.0.1
|
|
|
|
|
port=${project.ports.database}
|
|
|
|
|
socket=${join(directory, 'mariadb.sock')}
|
|
|
|
|
pid-file=${join(directory, 'pids', 'mariadb.pid')}
|
|
|
|
|
log-error=${join(directory, 'logs', 'mariadb.log')}
|
|
|
|
|
skip-name-resolve
|
|
|
|
|
`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function provisionNativeProject(project: NativeProjectDefinition): Promise<void> {
|
|
|
|
|
const directory = nativeDirectory(project.root)
|
2026-08-22 04:11:44 -05:00
|
|
|
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'
|
|
|
|
|
])
|
2026-08-22 03:47:09 -05:00
|
|
|
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)),
|
2026-08-22 04:26:37 -05:00
|
|
|
writeFile(join(directory, 'adminer.php'), renderNativeAdminerBootstrap(project)),
|
2026-08-22 04:11:44 -05:00
|
|
|
writeFile(
|
|
|
|
|
join(directory, 'config', 'mariadb.cnf'),
|
|
|
|
|
renderMariaDbConfig(project, installedRoot(project))
|
2026-08-26 20:54:58 -05:00
|
|
|
),
|
|
|
|
|
...(process.platform === 'win32'
|
|
|
|
|
? [
|
|
|
|
|
writeFile(
|
|
|
|
|
join(directory, 'config', 'php.ini'),
|
|
|
|
|
`extension_dir="${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="${join(directory, 'logs', 'php.log')}"\n`
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
: [])
|
2026-08-22 03:47:09 -05:00
|
|
|
])
|
|
|
|
|
try {
|
|
|
|
|
await access(join(directory, 'data', 'mariadb', 'mysql'))
|
|
|
|
|
} catch {
|
|
|
|
|
await execFileAsync(
|
2026-08-26 20:54:58 -05:00
|
|
|
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'
|
|
|
|
|
],
|
2026-08-22 03:47:09 -05:00
|
|
|
{ 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'),
|
2026-08-26 20:54:58 -05:00
|
|
|
command: runtimeExecutable(project, 'mariadbd'),
|
|
|
|
|
args: [
|
|
|
|
|
`--defaults-file=${join(directory, 'config', 'mariadb.cnf')}`,
|
|
|
|
|
...(process.platform === 'win32' ? ['--console'] : [])
|
|
|
|
|
],
|
2026-08-22 03:47:09 -05:00
|
|
|
ready: { port: project.ports.database, timeoutMs: 30000 }
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...common('php'),
|
2026-08-26 20:54:58 -05:00
|
|
|
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,
|
2026-08-22 03:47:09 -05:00
|
|
|
ready: { port: project.ports.php }
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...common('web'),
|
2026-08-26 20:54:58 -05:00
|
|
|
command: runtimeExecutable(project, 'nginx'),
|
2026-08-22 03:47:09 -05:00
|
|
|
args: ['-c', join(directory, 'config', 'nginx.conf'), '-p', `${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
|
|
|
|
|
}
|
|
|
|
|
}
|