From 1dd1c449bb68c5957e7bc384560df8dc411877f9 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:05:02 -0700 Subject: [PATCH] fix(desktop): run venv-blocker scan async off the main-process event loop The preflight's execFileSync froze the Electron UI event loop for up to 15s while psutil scanned the full process table. Convert scanVenvBlockers to async execFile and await it in applyUpdates; tests updated to async DI stubs. Also adds trailing newlines. --- apps/desktop/electron/main.ts | 2 +- .../electron/venv-blocker-scan.test.ts | 35 +++++++------ apps/desktop/electron/venv-blocker-scan.ts | 26 ++++++---- package-lock.json | 50 ++++++++++++++----- 4 files changed, 71 insertions(+), 42 deletions(-) diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index f2d3083e74d..3fe047aa71c 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -2891,7 +2891,7 @@ async function applyUpdates(opts = {}) { // malformed output, missing psutil) abort the handoff — never proceed // to the detached updater when the venv state is unknown. if (IS_WINDOWS) { - const scanOutcome = scanVenvBlockers(updateRoot) + const scanOutcome = await scanVenvBlockers(updateRoot) if (scanOutcome.kind === 'blocked') { const message = formatBlockerMessage(scanOutcome.result) diff --git a/apps/desktop/electron/venv-blocker-scan.test.ts b/apps/desktop/electron/venv-blocker-scan.test.ts index 3314848d702..164c291917b 100644 --- a/apps/desktop/electron/venv-blocker-scan.test.ts +++ b/apps/desktop/electron/venv-blocker-scan.test.ts @@ -165,46 +165,46 @@ describe('scanVenvBlockers', () => { }) function execReturn(json: string): any { - return ((...args: any[]) => json) as any + return (async (...args: any[]) => ({ stdout: json, stderr: '' })) as any } function execThrow(status: number, stderr: string): any { - return ((...args: any[]) => { const e: any = new Error(); e.status = status; e.stderr = Buffer.from(stderr); throw e }) as any + return (async (...args: any[]) => { const e: any = new Error(); e.status = status; e.stderr = Buffer.from(stderr); throw e }) as any } - it('clear scan returns clear', () => { - assert.equal(scanVenvBlockers('/r', execReturn(okJson), stubVenv).kind, 'clear') + it('clear scan returns clear', async () => { + assert.equal((await scanVenvBlockers('/r', execReturn(okJson), stubVenv)).kind, 'clear') }) - it('blocked scan returns blocked', () => { - assert.equal(scanVenvBlockers('/r', execReturn(blockedJson), stubVenv).kind, 'blocked') + it('blocked scan returns blocked', async () => { + assert.equal((await scanVenvBlockers('/r', execReturn(blockedJson), stubVenv)).kind, 'blocked') }) - it('non-zero exit is probe-failure', () => { - const o = scanVenvBlockers('/r', execThrow(2, 'ModuleNotFoundError'), stubVenv) + it('non-zero exit is probe-failure', async () => { + const o = await scanVenvBlockers('/r', execThrow(2, 'ModuleNotFoundError'), stubVenv) assert.equal(o.kind, 'probe-failure') }) - it('missing venv python is probe-failure', () => { - const o = scanVenvBlockers('/r', execReturn(okJson), () => null) + it('missing venv python is probe-failure', async () => { + const o = await scanVenvBlockers('/r', execReturn(okJson), () => null) assert.equal(o.kind, 'probe-failure') }) - it('malformed subprocess output is probe-failure', () => { - const o = scanVenvBlockers('/r', execReturn('bad json'), stubVenv) + it('malformed subprocess output is probe-failure', async () => { + const o = await scanVenvBlockers('/r', execReturn('bad json'), stubVenv) assert.equal(o.kind, 'probe-failure') }) - it('calls subprocess with correct args, cwd, timeout, stdio', () => { + it('calls subprocess with correct args, cwd and timeout', async () => { const calls: any[] = [] - const spy = ((cmd: string, args: string[], opts: any) => { - calls.push({ cmd, args, cwd: opts.cwd, timeout: opts.timeout, stdio: opts.stdio }) + const spy = (async (cmd: string, args: string[], opts: any) => { + calls.push({ cmd, args, cwd: opts.cwd, timeout: opts.timeout }) - return okJson + return { stdout: okJson, stderr: '' } }) as any - scanVenvBlockers('/update/root', spy, stubVenv) + await scanVenvBlockers('/update/root', spy, stubVenv) assert.equal(calls.length, 1) const c = calls[0] assert.ok(c.cmd.endsWith('python.exe')) @@ -212,6 +212,5 @@ describe('scanVenvBlockers', () => { assert.equal(c.cwd, '/update/root') assert.equal(typeof c.timeout, 'number') assert.ok(c.timeout > 0) - assert.deepEqual(c.stdio, ['ignore', 'pipe', 'pipe']) }) }) \ No newline at end of file diff --git a/apps/desktop/electron/venv-blocker-scan.ts b/apps/desktop/electron/venv-blocker-scan.ts index ec019ebab2b..67a6c1f7ce1 100644 --- a/apps/desktop/electron/venv-blocker-scan.ts +++ b/apps/desktop/electron/venv-blocker-scan.ts @@ -7,9 +7,12 @@ * returns a typed result for the Desktop update preflight. */ -import { execFileSync } from 'node:child_process' +import { execFile } from 'node:child_process' import fs from 'node:fs' import path from 'node:path' +import { promisify } from 'node:util' + +const execFileAsync = promisify(execFile) // --------------------------------------------------------------------------- // Types @@ -106,15 +109,17 @@ export function parseVenvBlockerScanOutput(raw: string): ScanOutcome { } /** - * Run the venv-blocker scan subprocess. Accepts optional overrides for - * testing (dependency injection). + * Run the venv-blocker scan subprocess. Async so the Electron main-process + * event loop is never blocked by the psutil process scan (up to 15s on a + * loaded Windows box). Accepts optional overrides for testing (dependency + * injection). */ -export function scanVenvBlockers( +export async function scanVenvBlockers( updateRoot: string, - execOverride?: typeof execFileSync, + execOverride?: typeof execFileAsync, resolveOverride?: typeof resolveVenvPython, -): ScanOutcome { - const execFn = execOverride || execFileSync +): Promise { + const execFn = execOverride || execFileAsync const resolveFn = resolveOverride || resolveVenvPython const venvPython = resolveFn(updateRoot) @@ -125,21 +130,20 @@ export function scanVenvBlockers( let stdout: string try { - const proc = execFn( + const proc = await execFn( venvPython, ['-m', SCAN_MODULE], { cwd: updateRoot, encoding: 'utf-8', - stdio: ['ignore', 'pipe', 'pipe'], timeout: SCAN_TIMEOUT_MS, windowsHide: true, } as any, ) - stdout = (proc as unknown as string) + stdout = String((proc as any).stdout ?? '') } catch (err: any) { - const diag = [`exit code ${err.status ?? -1}`] + const diag = [`exit code ${err.status ?? err.code ?? -1}`] if (err.stderr) {diag.push(String(err.stderr).slice(0, 200))} diff --git a/package-lock.json b/package-lock.json index e0031df8ff2..c4275fb8fff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -165,6 +165,7 @@ "@typescript-eslint/eslint-plugin": "^8.59.1", "@typescript-eslint/parser": "^8.59.1", "@vitejs/plugin-react": "^6.0.1", + "bippy": "0.5.43", "concurrently": "^10.0.3", "cross-env": "^10.1.0", "electron": "40.10.2", @@ -183,8 +184,7 @@ "typescript": "^6.0.3", "vite": "^8.0.10", "vitest": "^4.1.5", - "wait-on": "^9.0.5", - "bippy": "0.5.43" + "wait-on": "^9.0.5" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -1939,6 +1939,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=18" } @@ -1955,6 +1956,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -1971,6 +1973,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -1987,6 +1990,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -2003,6 +2007,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -2019,6 +2024,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -2035,6 +2041,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2051,6 +2058,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2067,6 +2075,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2083,6 +2092,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2099,6 +2109,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2115,6 +2126,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2131,6 +2143,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2147,6 +2160,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2163,6 +2177,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2179,6 +2194,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2195,6 +2211,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2211,6 +2228,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2227,6 +2245,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2243,6 +2262,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2259,6 +2279,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2275,6 +2296,7 @@ "os": [ "openharmony" ], + "peer": true, "engines": { "node": ">=18" } @@ -2291,6 +2313,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=18" } @@ -2307,6 +2330,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -2323,6 +2347,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -2339,6 +2364,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -7730,6 +7756,16 @@ "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==", "license": "MIT" }, + "node_modules/bippy": { + "version": "0.5.43", + "resolved": "https://registry.npmjs.org/bippy/-/bippy-0.5.43.tgz", + "integrity": "sha512-Tvu7b1M7+d8b9/YHaCeODEsi2CgbuoBql+dWSBrNnCuqJ1gMUeY3i0r+319hvjjl5GVBP6FFWxrKnq3fhZER0w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": ">=17.0.0" + } + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -19748,16 +19784,6 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" - }, - "node_modules/bippy": { - "version": "0.5.43", - "resolved": "https://registry.npmjs.org/bippy/-/bippy-0.5.43.tgz", - "integrity": "sha512-Tvu7b1M7+d8b9/YHaCeODEsi2CgbuoBql+dWSBrNnCuqJ1gMUeY3i0r+319hvjjl5GVBP6FFWxrKnq3fhZER0w==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react": ">=17.0.0" - } } } }