From 6a3d3ca7f8dd5aaefbbba57030905f796e10a7c1 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Fri, 10 Jul 2026 02:04:36 -0400 Subject: [PATCH] fix(build): address WSL runtime review findings --- .../scripts/electron-builder-before-build.cjs | 39 ++++++++++--------- .../wsl-watcher-runtime-build-source.mjs | 4 +- .../wsl-watcher-runtime-build-source.test.mjs | 37 ++++++++++++++++++ config/scripts/wsl-watcher-runtime-lock.mjs | 11 +++--- .../ipc/wsl-watcher-host-binding-watchdog.ts | 1 + 5 files changed, 67 insertions(+), 25 deletions(-) diff --git a/config/scripts/electron-builder-before-build.cjs b/config/scripts/electron-builder-before-build.cjs index b2a6169f76f..ed60b4d6e8c 100644 --- a/config/scripts/electron-builder-before-build.cjs +++ b/config/scripts/electron-builder-before-build.cjs @@ -51,29 +51,31 @@ function currentProcessStartToken() { try { if (process.platform === 'linux') { const contents = readFileSync(`/proc/${process.pid}/stat`, 'utf8') - cachedProcessStartToken = contents.slice(contents.lastIndexOf(') ') + 2).split(' ')[19] + cachedProcessStartToken = + contents.slice(contents.lastIndexOf(') ') + 2).split(' ')[19] || null } else if (process.platform === 'win32') { - cachedProcessStartToken = execFileSync( - 'powershell.exe', - [ - '-NoProfile', - '-NonInteractive', - '-Command', - `(Get-Process -Id ${process.pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks` - ], - { encoding: 'utf8', timeout: 2_000, windowsHide: true } - ).trim() + cachedProcessStartToken = + execFileSync( + 'powershell.exe', + [ + '-NoProfile', + '-NonInteractive', + '-Command', + `(Get-Process -Id ${process.pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks` + ], + { encoding: 'utf8', timeout: 2_000, windowsHide: true } + ).trim() || null } else { - cachedProcessStartToken = execFileSync( - 'ps', - ['-o', 'lstart=', '-p', String(process.pid)], - { encoding: 'utf8', timeout: 2_000 } - ).trim() + cachedProcessStartToken = + execFileSync('ps', ['-o', 'lstart=', '-p', String(process.pid)], { + encoding: 'utf8', + timeout: 2_000 + }).trim() || null } } catch { cachedProcessStartToken = null } - return cachedProcessStartToken || null + return cachedProcessStartToken } function parseWslRuntimeBuildSource(output) { @@ -83,8 +85,7 @@ function parseWslRuntimeBuildSource(output) { function parseWslRuntimePreparationOutput(output) { const text = Buffer.isBuffer(output) ? output.toString('utf8') : String(output ?? '') const lines = text.split(/\r?\n/) - const value = (name) => - lines.find((line) => line.startsWith(`${name}=`))?.slice(name.length + 1) + const value = (name) => lines.find((line) => line.startsWith(`${name}=`))?.slice(name.length + 1) const source = value('ORCA_WSL_WATCHER_BUILD_SOURCE') const lease = value('ORCA_WSL_WATCHER_BUILD_LEASE') if (!source || !lease) { diff --git a/config/scripts/wsl-watcher-runtime-build-source.mjs b/config/scripts/wsl-watcher-runtime-build-source.mjs index cfc8a29948a..5968c57fc69 100644 --- a/config/scripts/wsl-watcher-runtime-build-source.mjs +++ b/config/scripts/wsl-watcher-runtime-build-source.mjs @@ -199,9 +199,11 @@ async function activeLeaseVersions(buildsDir, names, operations, options) { let owner let modifiedAt = 0 try { - owner = JSON.parse(await operations.readFile(join(leasePath, 'owner.json'), 'utf8')) modifiedAt = (await operations.stat(leasePath)).mtimeMs } catch {} + try { + owner = JSON.parse(await operations.readFile(join(leasePath, 'owner.json'), 'utf8')) + } catch {} const alive = Number.isSafeInteger(owner?.pid) ? await options.ownerIsAlive(owner) : null if ( alive === true || diff --git a/config/scripts/wsl-watcher-runtime-build-source.test.mjs b/config/scripts/wsl-watcher-runtime-build-source.test.mjs index 7841454c52d..fa7e2e036b4 100644 --- a/config/scripts/wsl-watcher-runtime-build-source.test.mjs +++ b/config/scripts/wsl-watcher-runtime-build-source.test.mjs @@ -14,6 +14,43 @@ import { const execFileAsync = promisify(execFile) describe('immutable WSL runtime build retention', () => { + it('preserves a fresh lease while its owner metadata is being published', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-wsl-build-pending-lease-')) + const output = join(root, 'out', 'wsl-watcher') + const version = 'b'.repeat(20) + const buildsDir = `${output}.builds` + const build = join(buildsDir, version) + const lease = join(buildsDir, `${version}.lease-pending`) + try { + await mkdir(build, { recursive: true }) + await mkdir(lease) + await utimes(lease, new Date(99_500), new Date(99_500)) + + await pruneImmutableRuntimeBuilds(output, { + maxCompletedBuilds: 0, + minRecentBuilds: 0, + maxBuildAgeMs: 0, + unknownLeaseAgeMs: 1_000, + now: () => 100_000 + }) + expect(await readdir(buildsDir)).toEqual( + expect.arrayContaining([version, `${version}.lease-pending`]) + ) + + await pruneImmutableRuntimeBuilds(output, { + maxCompletedBuilds: 0, + minRecentBuilds: 0, + maxBuildAgeMs: 0, + unknownLeaseAgeMs: 1_000, + now: () => 101_000 + }) + expect(await readdir(buildsDir)).not.toContain(version) + expect(await readdir(buildsDir)).not.toContain(`${version}.lease-pending`) + } finally { + await rm(root, { recursive: true, force: true }) + } + }) + it('preserves current, recent, and actively leased builds while bounding completed versions', async () => { const root = await mkdtemp(join(tmpdir(), 'orca-wsl-build-retention-')) const output = join(root, 'out', 'wsl-watcher') diff --git a/config/scripts/wsl-watcher-runtime-lock.mjs b/config/scripts/wsl-watcher-runtime-lock.mjs index d1b389e2b38..e4c8939721f 100644 --- a/config/scripts/wsl-watcher-runtime-lock.mjs +++ b/config/scripts/wsl-watcher-runtime-lock.mjs @@ -1,6 +1,7 @@ import { randomUUID } from 'node:crypto' import { execFile } from 'node:child_process' import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises' +import { join } from 'node:path' import { promisify } from 'node:util' const DEFAULT_HEARTBEAT_INTERVAL_MS = 1_000 @@ -21,7 +22,7 @@ function delay(milliseconds) { async function readOwner(lockPath, operations) { try { - const parsed = JSON.parse(await operations.readFile(`${lockPath}/owner.json`, 'utf8')) + const parsed = JSON.parse(await operations.readFile(join(lockPath, 'owner.json'), 'utf8')) return typeof parsed?.token === 'string' && Number.isSafeInteger(parsed.pid) ? parsed : null } catch { return null @@ -78,7 +79,7 @@ async function defaultOwnerIsAlive(owner) { async function heartbeatIsStale(lockPath, operations, now, staleMs) { try { - const heartbeat = await operations.stat(`${lockPath}/heartbeat`) + const heartbeat = await operations.stat(join(lockPath, 'heartbeat')) return now() - heartbeat.mtimeMs >= staleMs } catch { try { @@ -125,7 +126,7 @@ function startHeartbeat(lockPath, owner, operations, intervalMs) { const tick = async () => { const current = await readOwner(lockPath, operations) if (current?.token === owner.token) { - await operations.writeFile(`${lockPath}/heartbeat`, owner.token).catch(() => undefined) + await operations.writeFile(join(lockPath, 'heartbeat'), owner.token).catch(() => undefined) } } const timer = setInterval(() => void tick(), intervalMs) @@ -163,8 +164,8 @@ export async function withRuntimePublicationLock( try { await operations.mkdir(lockPath) try { - await operations.writeFile(`${lockPath}/owner.json`, JSON.stringify(owner)) - await operations.writeFile(`${lockPath}/heartbeat`, token) + await operations.writeFile(join(lockPath, 'owner.json'), JSON.stringify(owner)) + await operations.writeFile(join(lockPath, 'heartbeat'), token) } catch (error) { await operations.rm(lockPath, { recursive: true, force: true }) throw error diff --git a/src/main/ipc/wsl-watcher-host-binding-watchdog.ts b/src/main/ipc/wsl-watcher-host-binding-watchdog.ts index ecb14596c77..bd183056f7f 100644 --- a/src/main/ipc/wsl-watcher-host-binding-watchdog.ts +++ b/src/main/ipc/wsl-watcher-host-binding-watchdog.ts @@ -7,6 +7,7 @@ export function createWatcherBindingWatchdog( timeoutMs: number ): WatcherBindingWatchdog { let recycling = false + // Why: concurrent native timeouts must trigger only one host recycle. const recycleHost = (operation: string): void => { if (recycling) { return