diff --git a/packages/supacloud-lite/scripts/package-smoke.ts b/packages/supacloud-lite/scripts/package-smoke.ts index 2441db545..4996e708f 100644 --- a/packages/supacloud-lite/scripts/package-smoke.ts +++ b/packages/supacloud-lite/scripts/package-smoke.ts @@ -2,6 +2,7 @@ import { access, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { dirname, join, resolve } from 'node:path' import packageJson from '../package.json' with { type: 'json' } +import { withWindowsSubprocessRef } from './subprocess.js' const packageDir = resolve(import.meta.dir, '..') const packDir = await mkdtemp(join(tmpdir(), 'supacloud-lite-pack-')) @@ -120,12 +121,14 @@ async function runCommandExpectingFailure(command: string[], cwd: string, env: N async function executeCommand(command: string[], cwd: string, env: NodeJS.ProcessEnv): Promise { const processHandle = Bun.spawn({ cmd: command, cwd, stdout: 'pipe', stderr: 'pipe', env }) - const [exitCode, stdout, stderr] = await Promise.all([ - processHandle.exited, - new Response(processHandle.stdout).text(), - new Response(processHandle.stderr).text(), - ]) - return { exitCode, stdout, stderr } + return await withWindowsSubprocessRef(async () => { + const [exitCode, stdout, stderr] = await Promise.all([ + processHandle.exited, + new Response(processHandle.stdout).text(), + new Response(processHandle.stderr).text(), + ]) + return { exitCode, stdout, stderr } + }) } interface CommandExecution { diff --git a/packages/supacloud-lite/scripts/standalone-smoke.ts b/packages/supacloud-lite/scripts/standalone-smoke.ts index f2b23cb21..9e37eb55b 100644 --- a/packages/supacloud-lite/scripts/standalone-smoke.ts +++ b/packages/supacloud-lite/scripts/standalone-smoke.ts @@ -2,6 +2,7 @@ import { access, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/p import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import packageJson from '../package.json' with { type: 'json' } +import { withWindowsSubprocessRef } from './subprocess.js' const packageDir = resolve(import.meta.dir, '..') const binary = resolveStandaloneBinary() @@ -225,7 +226,7 @@ async function withServer(options: ServerOptions, check: (url: string) => Promis } finally { console.log(`[standalone-smoke] ${options.phaseLabel}: stop`) processHandle.kill('SIGTERM') - const exitCode = await processHandle.exited + const exitCode = await withWindowsSubprocessRef(() => processHandle.exited) const expectedExitCode = expectedStandaloneShutdownExitCode() console.log( `[standalone-smoke] ${options.phaseLabel}: ${exitCode === expectedExitCode ? 'ok' : 'failed'} (exit ${exitCode})`, @@ -276,7 +277,7 @@ async function runCommand( const processHandle = Bun.spawn({ cmd: command, cwd, env, stdout: 'pipe', stderr: 'pipe' }) const stdoutPromise = new Response(processHandle.stdout).text() const stderrPromise = new Response(processHandle.stderr).text() - const exitCode = await processHandle.exited + const exitCode = await withWindowsSubprocessRef(() => processHandle.exited) console.log(`[standalone-smoke] ${commandLabel}: ${exitCode === 0 ? 'ok' : 'failed'} (exit ${exitCode})`) const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]) if (exitCode !== 0) throw new Error(`standalone command "${commandLabel}" failed (${exitCode})\n${stdout}\n${stderr}`) diff --git a/packages/supacloud-lite/scripts/subprocess.ts b/packages/supacloud-lite/scripts/subprocess.ts new file mode 100644 index 000000000..160c51f85 --- /dev/null +++ b/packages/supacloud-lite/scripts/subprocess.ts @@ -0,0 +1,13 @@ +/** + * Bun 1.3.14 stops polling Windows IOCP when only a subprocess exit is pending. + * Remove this compatibility hold after the stable runtime includes oven-sh/bun#34478. + */ +export async function withWindowsSubprocessRef(operation: () => Promise): Promise { + if (process.platform !== 'win32') return await operation() + const eventLoopRef = setInterval(() => {}, 1000) + try { + return await operation() + } finally { + clearInterval(eventLoopRef) + } +} diff --git a/packages/supacloud-lite/test/cli-shutdown.test.ts b/packages/supacloud-lite/test/cli-shutdown.test.ts index e237c7ffd..8762d92bc 100644 --- a/packages/supacloud-lite/test/cli-shutdown.test.ts +++ b/packages/supacloud-lite/test/cli-shutdown.test.ts @@ -4,6 +4,7 @@ import { access, mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { waitForShutdown } from '../src/shutdown.js' +import { withWindowsSubprocessRef } from '../scripts/subprocess.js' const cliPath = resolve(import.meta.dir, '../src/cli.ts') @@ -109,11 +110,11 @@ async function runCli(projectDir: string, command: string[]) { stdout: 'pipe', stderr: 'pipe', }) - const [exitCode, stdout, stderr] = await Promise.all([ + const [exitCode, stdout, stderr] = await withWindowsSubprocessRef(() => Promise.all([ processHandle.exited, new Response(processHandle.stdout).text(), new Response(processHandle.stderr).text(), - ]) + ])) return { exitCode, stdout, stderr, durationMs: performance.now() - startedAt } } @@ -123,7 +124,7 @@ async function stopCli(cliRun: ReturnType, signal: NodeJS.Signa } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error } - return await cliRun.processHandle.exited + return await withWindowsSubprocessRef(() => cliRun.processHandle.exited) } async function assertShutdownHandlerClosesProject(signal: NodeJS.Signals): Promise { diff --git a/packages/supacloud-lite/test/snapshot.test.ts b/packages/supacloud-lite/test/snapshot.test.ts index c8d9ee7d1..063d4e629 100644 --- a/packages/supacloud-lite/test/snapshot.test.ts +++ b/packages/supacloud-lite/test/snapshot.test.ts @@ -1,9 +1,10 @@ import { afterEach, describe, expect, test } from 'bun:test' -import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises' +import { access, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { ensureProjectSecrets, resolveProjectPaths } from '../src/project-runtime.js' import { createSnapshot, restoreSnapshot } from '../src/snapshot.js' +import { withWindowsSubprocessRef } from '../scripts/subprocess.js' import { createSymlinkIfPermitted } from './support/symlink.js' const temporaryDirectories: string[] = [] @@ -155,6 +156,10 @@ describe('Lite snapshots', () => { const status = await runCli(['status', '--project-dir', projectDir]) expect(status).toContain('20260728000000') + + const lockPath = `${resolveProjectPaths({ projectDir }).dataDir!}.supacloud-lite.lock` + await expect(access(lockPath)).rejects.toMatchObject({ code: 'ENOENT' }) + expect(await runCli(['status', '--project-dir', projectDir])).toContain('20260728000000') }) }) @@ -172,11 +177,13 @@ async function runCli(args: string[]): Promise { stderr: 'pipe', env: process.env, }) - const [exitCode, stdout, stderr] = await Promise.all([ - processHandle.exited, - new Response(processHandle.stdout).text(), - new Response(processHandle.stderr).text(), - ]) - if (exitCode !== 0) throw new Error(`CLI failed (${exitCode}): ${stderr || stdout}`) - return stdout + return await withWindowsSubprocessRef(async () => { + const [exitCode, stdout, stderr] = await Promise.all([ + processHandle.exited, + new Response(processHandle.stdout).text(), + new Response(processHandle.stderr).text(), + ]) + if (exitCode !== 0) throw new Error(`CLI failed (${exitCode}): ${stderr || stdout}`) + return stdout + }) }