fix: harden Windows native project startup

This commit is contained in:
reaper
2026-08-28 01:24:27 -05:00
parent ff38e91a70
commit a942076724
8 changed files with 71 additions and 22 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ on:
release_tag: release_tag:
description: Existing GitHub release tag to receive the macOS files description: Existing GitHub release tag to receive the macOS files
required: true required: true
default: v2.0.0-alpha.30 default: v2.0.0-alpha.31
push: push:
tags: tags:
- 'v*' - 'v*'
+5 -1
View File
@@ -6,7 +6,7 @@ on:
release_tag: release_tag:
description: Existing GitHub release tag to receive the Windows installer description: Existing GitHub release tag to receive the Windows installer
required: true required: true
default: v2.0.0-alpha.30 default: v2.0.0-alpha.31
push: push:
tags: tags:
- 'v*' - 'v*'
@@ -42,6 +42,10 @@ jobs:
run: | run: |
./runtime-build/windows-x64/stage-runtime.ps1 ./runtime-build/windows-x64/stage-runtime.ps1
node scripts/smoke-native-runtime.cjs dist/native-runtime-stage/win32-x64 node scripts/smoke-native-runtime.cjs dist/native-runtime-stage/win32-x64
$runtime = (Resolve-Path 'dist/native-runtime-stage/win32-x64').Path
$env:AURORA_NATIVE_SMOKE_ROOT = $runtime
$env:AURORA_NATIVE_WORDPRESS_SMOKE_ROOT = $runtime
npx vitest run src/main/native/nativeProject.test.ts
node scripts/package-native-runtime.cjs dist/native-runtime-stage/win32-x64 dist/native-runtime/aurora-native-0.1.0-win32-x64.tar.gz node scripts/package-native-runtime.cjs dist/native-runtime-stage/win32-x64 dist/native-runtime/aurora-native-0.1.0-win32-x64.tar.gz
- name: Read application version - name: Read application version
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "aurora-dockside", "name": "aurora-dockside",
"version": "2.0.0-alpha.30", "version": "2.0.0-alpha.31",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "aurora-dockside", "name": "aurora-dockside",
"version": "2.0.0-alpha.30", "version": "2.0.0-alpha.31",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/preload": "^3.0.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aurora-dockside", "name": "aurora-dockside",
"version": "2.0.0-alpha.30", "version": "2.0.0-alpha.31",
"description": "A modern, modular Docker development platform for building, running, and managing web applications locally.", "description": "A modern, modular Docker development platform for building, running, and managing web applications locally.",
"desktopName": "aurora-dockside.desktop", "desktopName": "aurora-dockside.desktop",
"main": "./out/main/index.js", "main": "./out/main/index.js",
+11
View File
@@ -38,6 +38,15 @@ function sendExit(sender: WebContents, operationId: string, exitCode: number): v
sender.send('terminal:exit', { operationId, exitCode, cancelled: false }) sender.send('terminal:exit', { operationId, exitCode, cancelled: false })
} }
function sendError(sender: WebContents, operationId: string, error: unknown): void {
if (sender.isDestroyed()) return
sender.send('terminal:data', {
operationId,
stream: 'stderr',
chunk: `\n${error instanceof Error ? error.message : String(error)}\n`
})
}
function loadModule(moduleId: string, main: string): LoadedModule { function loadModule(moduleId: string, main: string): LoadedModule {
const root = resolve(moduleDirectory(), moduleId) const root = resolve(moduleDirectory(), moduleId)
const entry = resolve(root, main) const entry = resolve(root, main)
@@ -186,6 +195,7 @@ export async function runModuleProjectCreate(
}) })
sendExit(sender, operationId, 0) sendExit(sender, operationId, 0)
} catch (error) { } catch (error) {
sendError(sender, operationId, error)
sendExit(sender, operationId, 1) sendExit(sender, operationId, 1)
throw error throw error
} }
@@ -216,6 +226,7 @@ export async function runModuleProjectTool(
if (!handled) throw new Error(`Module '${moduleId}' does not implement projectTool`) if (!handled) throw new Error(`Module '${moduleId}' does not implement projectTool`)
sendExit(sender, operationId, 0) sendExit(sender, operationId, 0)
} catch (error) { } catch (error) {
sendError(sender, operationId, error)
sendExit(sender, operationId, 1) sendExit(sender, operationId, 1)
throw error throw error
} }
+25 -6
View File
@@ -7,6 +7,7 @@ import { promisify } from 'util'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { import {
renderMariaDbConfig, renderMariaDbConfig,
nativeConfigPath,
renderNativeAdminerBootstrap, renderNativeAdminerBootstrap,
renderNginxConfig, renderNginxConfig,
renderPhpFpmConfig, renderPhpFpmConfig,
@@ -25,6 +26,12 @@ const project = {
} }
const execFileAsync = promisify(execFile) const execFileAsync = promisify(execFile)
function runtimeCommand(runtime: string, name: string): string {
if (process.platform !== 'win32') return join(runtime, 'bin', name)
if (name === 'php') return join(runtime, 'bin', 'php', 'php.exe')
return join(runtime, 'bin', 'mariadb', 'bin', `${name}.exe`)
}
function runWithInput(command: string, args: string[], cwd: string, input: string): Promise<void> { function runWithInput(command: string, args: string[], cwd: string, input: string): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const child = spawn(command, args, { cwd, env: process.env, stdio: ['pipe', 'pipe', 'pipe'] }) const child = spawn(command, args, { cwd, env: process.env, stdio: ['pipe', 'pipe', 'pipe'] })
@@ -42,6 +49,11 @@ function runWithInput(command: string, args: string[], cwd: string, input: strin
} }
describe('native project configuration', () => { describe('native project configuration', () => {
it('writes portable forward-slash paths into Windows service configuration', () => {
expect(nativeConfigPath('C:\\Users\\Aurora Dragon\\project', 'win32')).toBe(
'C:/Users/Aurora Dragon/project'
)
})
it('isolates PHP-FPM on its allocated loopback port', () => it('isolates PHP-FPM on its allocated loopback port', () =>
expect(renderPhpFpmConfig(project)).toContain('listen = 127.0.0.1:41002')) expect(renderPhpFpmConfig(project)).toContain('listen = 127.0.0.1:41002'))
it('routes nginx PHP requests to the project PHP-FPM service', () => { it('routes nginx PHP requests to the project PHP-FPM service', () => {
@@ -131,8 +143,15 @@ it.runIf(Boolean(process.env.AURORA_NATIVE_WORDPRESS_SMOKE_ROOT))(
https: `http://127.0.0.1:${definition.ports.http}` https: `http://127.0.0.1:${definition.ports.http}`
}, },
native: { native: {
php: join(runtime, 'bin', 'php'), php: runtimeCommand(runtime, 'php'),
wp: join(runtime, 'bin', 'wp'), wp:
process.platform === 'win32'
? runtimeCommand(runtime, 'php')
: join(runtime, 'bin', 'wp'),
wpPrefixArgs:
process.platform === 'win32'
? ['-d', 'memory_limit=512M', join(runtime, 'tools', 'wp-cli.phar')]
: [],
databasePort: definition.ports.database, databasePort: definition.ports.database,
start: () => startNativeProject(definition) start: () => startNativeProject(definition)
}, },
@@ -161,12 +180,12 @@ it.runIf(Boolean(process.env.AURORA_NATIVE_WORDPRESS_SMOKE_ROOT))(
'db' 'db'
] ]
const dump = await execFileAsync( const dump = await execFileAsync(
join(runtime, 'bin', 'mariadb-dump'), runtimeCommand(runtime, 'mariadb-dump'),
[...databaseArgs.slice(0, -1), '--single-transaction', 'db'], [...databaseArgs.slice(0, -1), '--single-transaction', 'db'],
{ cwd: root, env: process.env, maxBuffer: 32 * 1024 * 1024 } { cwd: root, env: process.env, maxBuffer: 32 * 1024 * 1024 }
) )
await execFileAsync( await execFileAsync(
join(runtime, 'bin', 'mariadb'), runtimeCommand(runtime, 'mariadb'),
[ [
...databaseArgs, ...databaseArgs,
'-e', '-e',
@@ -174,9 +193,9 @@ it.runIf(Boolean(process.env.AURORA_NATIVE_WORDPRESS_SMOKE_ROOT))(
], ],
{ cwd: root, env: process.env } { cwd: root, env: process.env }
) )
await runWithInput(join(runtime, 'bin', 'mariadb'), databaseArgs, root, dump.stdout) await runWithInput(runtimeCommand(runtime, 'mariadb'), databaseArgs, root, dump.stdout)
const restored = await execFileAsync( const restored = await execFileAsync(
join(runtime, 'bin', 'mariadb'), runtimeCommand(runtime, 'mariadb'),
[ [
...databaseArgs, ...databaseArgs,
'--batch', '--batch',
+19 -10
View File
@@ -34,7 +34,11 @@ function nativeDirectory(root: string): string {
} }
function quoteNginx(value: string): string { function quoteNginx(value: string): string {
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"') 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 { export function renderPhpFpmConfig(project: NativeProjectDefinition): string {
@@ -152,15 +156,15 @@ export function renderMariaDbConfig(
process.platform === 'win32' process.platform === 'win32'
? join(installedRuntimeRoot, 'bin', 'mariadb') ? join(installedRuntimeRoot, 'bin', 'mariadb')
: join(installedRuntimeRoot, 'root', 'usr') : join(installedRuntimeRoot, 'root', 'usr')
const configPath = (value: string): string => nativeConfigPath(value)
return `[mariadbd] return `[mariadbd]
basedir=${basedir} basedir="${configPath(basedir)}"
datadir=${join(directory, 'data', 'mariadb')} datadir="${configPath(join(directory, 'data', 'mariadb'))}"
tmpdir=${join(directory, 'tmp')} tmpdir="${configPath(join(directory, 'tmp'))}"
bind-address=127.0.0.1 bind-address=127.0.0.1
port=${project.ports.database} port=${project.ports.database}
socket=${join(directory, 'mariadb.sock')} ${process.platform === 'win32' ? '' : `socket=${join(directory, 'mariadb.sock')}\n`}pid-file="${configPath(join(directory, 'pids', 'mariadb.pid'))}"
pid-file=${join(directory, 'pids', 'mariadb.pid')} log-error="${configPath(join(directory, 'logs', 'mariadb.log'))}"
log-error=${join(directory, 'logs', 'mariadb.log')}
skip-name-resolve skip-name-resolve
` `
} }
@@ -192,7 +196,7 @@ export async function provisionNativeProject(project: NativeProjectDefinition):
? [ ? [
writeFile( writeFile(
join(directory, 'config', 'php.ini'), 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` `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`
) )
] ]
: []) : [])
@@ -229,7 +233,7 @@ export function nativeServiceSpecs(project: NativeProjectDefinition): NativeServ
...common('database'), ...common('database'),
command: runtimeExecutable(project, 'mariadbd'), command: runtimeExecutable(project, 'mariadbd'),
args: [ args: [
`--defaults-file=${join(directory, 'config', 'mariadb.cnf')}`, `--defaults-file=${nativeConfigPath(join(directory, 'config', 'mariadb.cnf'))}`,
...(process.platform === 'win32' ? ['--console'] : []) ...(process.platform === 'win32' ? ['--console'] : [])
], ],
ready: { port: project.ports.database, timeoutMs: 30000 } ready: { port: project.ports.database, timeoutMs: 30000 }
@@ -253,7 +257,12 @@ export function nativeServiceSpecs(project: NativeProjectDefinition): NativeServ
{ {
...common('web'), ...common('web'),
command: runtimeExecutable(project, 'nginx'), command: runtimeExecutable(project, 'nginx'),
args: ['-c', join(directory, 'config', 'nginx.conf'), '-p', `${directory}/`], args: [
'-c',
nativeConfigPath(join(directory, 'config', 'nginx.conf')),
'-p',
`${nativeConfigPath(directory)}/`
],
ready: { port: project.ports.http } ready: { port: project.ports.http }
} }
] ]
+7 -1
View File
@@ -144,7 +144,13 @@ export class NativeProcessSupervisor {
await terminate(child.pid) await terminate(child.pid)
this.children.delete(spec.id) this.children.delete(spec.id)
await rm(spec.pidPath, { force: true }) await rm(spec.pidPath, { force: true })
throw error const reason = error instanceof Error ? error.message : String(error)
const details = log.trim()
throw new Error(
details
? `${spec.id} failed to start: ${reason}\n\n${details}`
: `${spec.id} failed to start: ${reason} See ${spec.logPath}`
)
} }
} }