diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml index 0e18ef3..11be0f5 100644 --- a/.github/workflows/linux-release.yml +++ b/.github/workflows/linux-release.yml @@ -38,7 +38,7 @@ jobs: test -f dist/native-runtime/aurora-native-0.1.0-linux-x64.tar.gz test -f dist/linux-unpacked/resources/native-runtime/aurora-native-0.1.0-linux-x64.tar.gz cmp dist/native-runtime/aurora-native-0.1.0-linux-x64.tar.gz dist/linux-unpacked/resources/native-runtime/aurora-native-0.1.0-linux-x64.tar.gz - test -f dist/linux-unpacked/resources/module-catalog/wordpress-1.2.0.pac + test -f dist/linux-unpacked/resources/module-catalog/wordpress-1.2.1.pac test -f dist/linux-unpacked/resources/module-catalog/drupal-1.0.0.pac - name: Create checksums diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml index 950ba90..e655909 100644 --- a/.github/workflows/macos-release.yml +++ b/.github/workflows/macos-release.yml @@ -76,10 +76,10 @@ jobs: - name: Verify embedded PAC catalog run: | - test -f dist/module-catalog/wordpress-1.2.0.pac - test -f "dist/mac-universal/Aurora Dockside.app/Contents/Resources/module-catalog/wordpress-1.2.0.pac" - cmp dist/module-catalog/wordpress-1.2.0.pac "dist/mac-universal/Aurora Dockside.app/Contents/Resources/module-catalog/wordpress-1.2.0.pac" - unzip -t "dist/mac-universal/Aurora Dockside.app/Contents/Resources/module-catalog/wordpress-1.2.0.pac" + test -f dist/module-catalog/wordpress-1.2.1.pac + test -f "dist/mac-universal/Aurora Dockside.app/Contents/Resources/module-catalog/wordpress-1.2.1.pac" + cmp dist/module-catalog/wordpress-1.2.1.pac "dist/mac-universal/Aurora Dockside.app/Contents/Resources/module-catalog/wordpress-1.2.1.pac" + unzip -t "dist/mac-universal/Aurora Dockside.app/Contents/Resources/module-catalog/wordpress-1.2.1.pac" - name: Create checksums run: | diff --git a/package-lock.json b/package-lock.json index e634a0e..0b30245 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aurora-dockside", - "version": "2.0.0-alpha.36", + "version": "2.0.0-alpha.37", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aurora-dockside", - "version": "2.0.0-alpha.36", + "version": "2.0.0-alpha.37", "hasInstallScript": true, "dependencies": { "@electron-toolkit/preload": "^3.0.2", diff --git a/package.json b/package.json index 402fe3c..010bea3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aurora-dockside", - "version": "2.0.0-alpha.36", + "version": "2.0.0-alpha.37", "description": "A modern, modular Docker development platform for building, running, and managing web applications locally.", "desktopName": "aurora-dockside.desktop", "main": "./out/main/index.js", diff --git a/packages/aurora-module-wordpress/main/index.cjs b/packages/aurora-module-wordpress/main/index.cjs index eca01fc..786d6a1 100644 --- a/packages/aurora-module-wordpress/main/index.cjs +++ b/packages/aurora-module-wordpress/main/index.cjs @@ -44,12 +44,23 @@ exports.projectCreate = async function projectCreate(context) { const networkBase = native ? base : wpArgs(context, true) const command = native ? context.native.wp : 'docker' const siteUrl = native ? context.urls.http : context.urls.https - const wp = (label, args) => context.run(label, command, [...networkBase, ...args]) + const wp = (label, args) => + context.run( + label, + command, + [...networkBase, ...args], + native ? context.native.commandEnvironment : undefined + ) if (native) { await context.native.start() const port = Number(context.native.databasePort) const bootstrap = `$db=new mysqli('127.0.0.1','root','',null,${port});if($db->connect_error)throw new Exception($db->connect_error);$db->query('CREATE DATABASE IF NOT EXISTS db');$db->query("CREATE USER IF NOT EXISTS 'db'@'127.0.0.1' IDENTIFIED BY 'db'");$db->query("GRANT ALL ON db.* TO 'db'@'127.0.0.1'");` - await context.run('database', context.native.php, [...nativePhpPrefix, '-r', bootstrap]) + await context.run( + 'database', + context.native.php, + [...nativePhpPrefix, '-r', bootstrap], + context.native.commandEnvironment + ) } else { await context.ensureRouter() await context.run('start', 'docker', [ @@ -62,24 +73,28 @@ exports.projectCreate = async function projectCreate(context) { '--remove-orphans' ]) } - await context.run('download', command, [ - ...base, - 'core', + await context.run( 'download', - `--locale=${s.locale || 'en_US'}`, - '--force' - ]) - await context.run('config', command, [ - ...networkBase, + command, + [...base, 'core', 'download', `--locale=${s.locale || 'en_US'}`, '--force'], + native ? context.native.commandEnvironment : undefined + ) + await context.run( 'config', - 'create', - '--dbname=db', - '--dbuser=db', - '--dbpass=db', - `--dbhost=${native ? `127.0.0.1:${context.native.databasePort}` : 'db:3306'}`, - '--skip-check', - '--force' - ]) + command, + [ + ...networkBase, + 'config', + 'create', + '--dbname=db', + '--dbuser=db', + '--dbpass=db', + `--dbhost=${native ? `127.0.0.1:${context.native.databasePort}` : 'db:3306'}`, + '--skip-check', + '--force' + ], + native ? context.native.commandEnvironment : undefined + ) await wp('install', [ 'core', 'install', diff --git a/src/main/commandRunner.ts b/src/main/commandRunner.ts index c0cd9c9..019f332 100644 --- a/src/main/commandRunner.ts +++ b/src/main/commandRunner.ts @@ -1,11 +1,88 @@ import { spawn, type ChildProcess } from 'child_process' import type { WebContents } from 'electron' import { AURORA_ENV, powerOffProjects } from './auroraEngine' -const running = new Map(); const cancelledIds = new Set() -const ANSI_PATTERN = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))/g -const stripAnsi=(s:string)=>s.replace(ANSI_PATTERN,'') -export function runCommandStreamed(operationId:string, command:string, args:string[], sender:WebContents, options:{cwd?:string;emitExit?:boolean}={}):Promise{return new Promise((resolve,reject)=>{const child=spawn(command,args,{env:AURORA_ENV,cwd:options.cwd,stdio:['ignore','pipe','pipe']});running.set(operationId,child);const tail:string[]=[];const forward=(stream:'stdout'|'stderr')=>(data:Buffer)=>{const chunk=stripAnsi(data.toString());tail.push(chunk);if(tail.length>20)tail.shift();if(!sender.isDestroyed())sender.send('terminal:data',{operationId,stream,chunk})};child.stdout.on('data',forward('stdout'));child.stderr.on('data',forward('stderr'));child.on('error',e=>{running.delete(operationId);if(options.emitExit!==false&&!sender.isDestroyed())sender.send('terminal:exit',{operationId,exitCode:null,cancelled:false});reject(e)});child.on('close',code=>{running.delete(operationId);const cancelled=cancelledIds.delete(operationId);if(options.emitExit!==false&&!sender.isDestroyed())sender.send('terminal:exit',{operationId,exitCode:code,cancelled});if(cancelled)reject(new Error('Command cancelled'));else if(code===0)resolve();else reject(new Error(tail.join('').trim()||`${command} exited with code ${code}`))})})} -export function cancelCommand(id:string):boolean{const c=running.get(id);if(!c)return false;cancelledIds.add(id);c.kill();return true} -export function startLogStream(operationId:string, command:string,args:string[],sender:WebContents,options:{cwd?:string}={}):void{const child=spawn(command,args,{env:AURORA_ENV,cwd:options.cwd,stdio:['ignore','pipe','pipe']});running.set(operationId,child);const f=(stream:'stdout'|'stderr')=>(d:Buffer)=>{if(!sender.isDestroyed())sender.send('logs:data',{operationId,stream,chunk:stripAnsi(d.toString())})};child.stdout.on('data',f('stdout'));child.stderr.on('data',f('stderr'));const done=()=>{running.delete(operationId);if(!sender.isDestroyed())sender.send('logs:exit',{operationId})};child.on('close',done);child.on('error',done)} -export function killAllRunningCommands():void{for(const c of running.values())c.kill();running.clear()} -export async function powerOffAllProjects():Promise{await powerOffProjects()} +const running = new Map() +const cancelledIds = new Set() +const ANSI_PATTERN = + /[\u001B\u009B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))/g +const stripAnsi = (s: string) => s.replace(ANSI_PATTERN, '') +export function runCommandStreamed( + operationId: string, + command: string, + args: string[], + sender: WebContents, + options: { cwd?: string; emitExit?: boolean; env?: NodeJS.ProcessEnv } = {} +): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + env: { ...AURORA_ENV, ...options.env }, + cwd: options.cwd, + stdio: ['ignore', 'pipe', 'pipe'] + }) + running.set(operationId, child) + const tail: string[] = [] + const forward = (stream: 'stdout' | 'stderr') => (data: Buffer) => { + const chunk = stripAnsi(data.toString()) + tail.push(chunk) + if (tail.length > 20) tail.shift() + if (!sender.isDestroyed()) sender.send('terminal:data', { operationId, stream, chunk }) + } + child.stdout.on('data', forward('stdout')) + child.stderr.on('data', forward('stderr')) + child.on('error', (e) => { + running.delete(operationId) + if (options.emitExit !== false && !sender.isDestroyed()) + sender.send('terminal:exit', { operationId, exitCode: null, cancelled: false }) + reject(e) + }) + child.on('close', (code) => { + running.delete(operationId) + const cancelled = cancelledIds.delete(operationId) + if (options.emitExit !== false && !sender.isDestroyed()) + sender.send('terminal:exit', { operationId, exitCode: code, cancelled }) + if (cancelled) reject(new Error('Command cancelled')) + else if (code === 0) resolve() + else reject(new Error(tail.join('').trim() || `${command} exited with code ${code}`)) + }) + }) +} +export function cancelCommand(id: string): boolean { + const c = running.get(id) + if (!c) return false + cancelledIds.add(id) + c.kill() + return true +} +export function startLogStream( + operationId: string, + command: string, + args: string[], + sender: WebContents, + options: { cwd?: string } = {} +): void { + const child = spawn(command, args, { + env: AURORA_ENV, + cwd: options.cwd, + stdio: ['ignore', 'pipe', 'pipe'] + }) + running.set(operationId, child) + const f = (stream: 'stdout' | 'stderr') => (d: Buffer) => { + if (!sender.isDestroyed()) + sender.send('logs:data', { operationId, stream, chunk: stripAnsi(d.toString()) }) + } + child.stdout.on('data', f('stdout')) + child.stderr.on('data', f('stderr')) + const done = () => { + running.delete(operationId) + if (!sender.isDestroyed()) sender.send('logs:exit', { operationId }) + } + child.on('close', done) + child.on('error', done) +} +export function killAllRunningCommands(): void { + for (const c of running.values()) c.kill() + running.clear() +} +export async function powerOffAllProjects(): Promise { + await powerOffProjects() +} diff --git a/src/main/moduleRuntime.ts b/src/main/moduleRuntime.ts index 25af374..aff43d8 100644 --- a/src/main/moduleRuntime.ts +++ b/src/main/moduleRuntime.ts @@ -1,5 +1,6 @@ import { execFile } from 'child_process' import { createRequire } from 'module' +import { mkdir } from 'fs/promises' import { promisify } from 'util' import { dirname, join, resolve, sep } from 'path' import type { WebContents } from 'electron' @@ -78,7 +79,18 @@ export async function runModuleLifecycleHook( : options.projectName ? projectUrls(options.projectName) : undefined - const run = async (label: string, command: string, args: string[]): Promise => { + const nativeTemp = + nativeDefinition && process.platform === 'win32' ? join(runtimeRoot(), 'tmp') : undefined + if (nativeTemp) await mkdir(nativeTemp, { recursive: true }) + const nativeCommandEnv = nativeTemp + ? { TEMP: nativeTemp, TMP: nativeTemp, WP_CLI_CACHE_DIR: join(nativeTemp, 'wp-cli-cache') } + : undefined + const run = async ( + label: string, + command: string, + args: string[], + env?: NodeJS.ProcessEnv + ): Promise => { if (options.sender && options.operationId) { if (!options.sender.isDestroyed()) options.sender.send('terminal:data', { @@ -88,12 +100,13 @@ export async function runModuleLifecycleHook( }) return runCommandStreamed(options.operationId, command, args, options.sender, { cwd: directory ?? resolve(moduleDirectory(), moduleId), - emitExit: false + emitExit: false, + env }) } await execFileAsync(command, args, { cwd: directory ?? resolve(moduleDirectory(), moduleId), - env: AURORA_ENV + env: { ...AURORA_ENV, ...env } }) } await handler( @@ -122,6 +135,7 @@ export async function runModuleLifecycleHook( process.platform === 'win32' ? ['-c', join(directory!, '.aurora', 'native', 'config', 'php.ini')] : [], + commandEnvironment: nativeCommandEnv, php: process.platform === 'win32' ? join(runtimeRoot(), 'bin', 'php', 'php.exe') diff --git a/src/main/native/nativeProject.test.ts b/src/main/native/nativeProject.test.ts index a4a41fb..2583eca 100644 --- a/src/main/native/nativeProject.test.ts +++ b/src/main/native/nativeProject.test.ts @@ -1,6 +1,6 @@ import { execFile, spawn } from 'child_process' import { createRequire } from 'module' -import { mkdtemp, readFile, writeFile } from 'fs/promises' +import { mkdir, mkdtemp, readFile, writeFile } from 'fs/promises' import { tmpdir } from 'os' import { dirname, join, resolve } from 'path' import { promisify } from 'util' @@ -128,6 +128,8 @@ it.runIf(Boolean(process.env.AURORA_NATIVE_WORDPRESS_SMOKE_ROOT))( async () => { const runtime = process.env.AURORA_NATIVE_WORDPRESS_SMOKE_ROOT! const root = await mkdtemp(join(dirname(runtime), 'aurora-native-wordpress-')) + const commandTemp = join(dirname(runtime), 'wp-cli-temp') + await mkdir(commandTemp, { recursive: true }) const definition = { name: `wordpress-${Date.now()}`, root, @@ -158,6 +160,11 @@ it.runIf(Boolean(process.env.AURORA_NATIVE_WORDPRESS_SMOKE_ROOT))( https: `http://127.0.0.1:${definition.ports.http}` }, native: { + commandEnvironment: { + TEMP: commandTemp, + TMP: commandTemp, + WP_CLI_CACHE_DIR: join(commandTemp, 'cache') + }, phpPrefixArgs: process.platform === 'win32' ? ['-c', join(root, '.aurora', 'native', 'config', 'php.ini')] @@ -180,10 +187,10 @@ it.runIf(Boolean(process.env.AURORA_NATIVE_WORDPRESS_SMOKE_ROOT))( databasePort: definition.ports.database, start: () => startNativeProject(definition) }, - run: async (_label: string, command: string, args: string[]) => { + run: async (_label: string, command: string, args: string[], env?: NodeJS.ProcessEnv) => { await execFileAsync(command, args, { cwd: root, - env: process.env, + env: { ...process.env, ...env }, maxBuffer: 32 * 1024 * 1024 }) },