From 89b64d9dd5411e40092d4417b8d77ce911092828 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 08:37:43 +0200 Subject: [PATCH] fix(coding-agent): avoid Windows self-update native locks closes #4157 --- packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/src/main.ts | 7 +- .../coding-agent/src/package-manager-cli.ts | 24 ++++++ .../src/utils/windows-self-update.ts | 84 +++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 packages/coding-agent/src/utils/windows-self-update.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 969d9d32..822b83ec 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Windows npm self-updates to move loaded native dependency packages out of the active install before reinstalling pi ([#4157](https://github.com/earendil-works/pi/issues/4157)). + ## [0.75.1] - 2026-05-18 ### Fixed diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 72e502be..9f1af697 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -15,7 +15,7 @@ import { processFileArguments } from "./cli/file-processor.js"; import { buildInitialMessage } from "./cli/initial-message.js"; import { listModels } from "./cli/list-models.js"; import { selectSession } from "./cli/session-picker.js"; -import { ENV_SESSION_DIR, expandTildePath, getAgentDir, VERSION } from "./config.js"; +import { ENV_SESSION_DIR, expandTildePath, getAgentDir, getPackageDir, VERSION } from "./config.js"; import { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from "./core/agent-session-runtime.js"; import { type AgentSessionRuntimeDiagnostic, @@ -46,6 +46,7 @@ import { ExtensionSelectorComponent } from "./modes/interactive/components/exten import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js"; import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.js"; import { isLocalPath } from "./utils/paths.js"; +import { cleanupWindowsSelfUpdateQuarantine } from "./utils/windows-self-update.js"; /** * Read all content from piped stdin. @@ -428,6 +429,10 @@ export async function main(args: string[], options?: MainOptions) { process.env.PI_SKIP_VERSION_CHECK = "1"; } + if (process.platform === "win32") { + cleanupWindowsSelfUpdateQuarantine(getPackageDir()); + } + if (await handlePackageCommand(args)) { return; } diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index f421af32..a3ef1e21 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -3,7 +3,9 @@ import { spawn } from "child_process"; import { selectConfig } from "./cli/config-selector.js"; import { APP_NAME, + detectInstallMethod, getAgentDir, + getPackageDir, getSelfUpdateCommand, getSelfUpdateUnavailableInstruction, PACKAGE_NAME, @@ -14,6 +16,10 @@ import { DefaultPackageManager } from "./core/package-manager.js"; import { SettingsManager } from "./core/settings-manager.js"; import { resolveSpawnCommand } from "./utils/child-process.js"; import { getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.js"; +import { + cleanupWindowsSelfUpdateQuarantine, + quarantineWindowsNativeDependencies, +} from "./utils/windows-self-update.js"; export type PackageCommand = "install" | "remove" | "update" | "list"; @@ -336,6 +342,16 @@ async function runSelfUpdate(command: SelfUpdateCommand): Promise { } } +function prepareWindowsNpmSelfUpdate(): void { + if (process.platform !== "win32") { + return; + } + + const packageDir = getPackageDir(); + cleanupWindowsSelfUpdateQuarantine(packageDir); + quarantineWindowsNativeDependencies(packageDir); +} + export async function handleConfigCommand(args: string[]): Promise { if (args[0] !== "config") { return false; @@ -489,6 +505,13 @@ export async function handlePackageCommand(args: string[]): Promise { if (!selfUpdatePlan.shouldRun) { return true; } + const installMethod = detectInstallMethod(); + if (process.platform === "win32" && installMethod !== "npm") { + console.error(chalk.red(`${APP_NAME} self-update on Windows is only supported for npm installs.`)); + console.error(chalk.dim(`Detected install method: ${installMethod}. Update ${APP_NAME} manually.`)); + process.exitCode = 1; + return true; + } const selfUpdateCommand = getSelfUpdateCommand( PACKAGE_NAME, selfUpdateNpmCommand, @@ -500,6 +523,7 @@ export async function handlePackageCommand(args: string[]): Promise { return true; } try { + prepareWindowsNpmSelfUpdate(); await runSelfUpdate(selfUpdateCommand); } catch (error: unknown) { const message = error instanceof Error ? error.message : "Unknown package command error"; diff --git a/packages/coding-agent/src/utils/windows-self-update.ts b/packages/coding-agent/src/utils/windows-self-update.ts new file mode 100644 index 00000000..a988529e --- /dev/null +++ b/packages/coding-agent/src/utils/windows-self-update.ts @@ -0,0 +1,84 @@ +import { randomUUID } from "node:crypto"; +import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync } from "node:fs"; +import { basename, dirname, join, relative, resolve, toNamespacedPath } from "node:path"; +import { getCwdRelativePath } from "./paths.js"; + +const QUARANTINE_DIR_NAME = ".pi-native-quarantine"; + +function normalizePath(path: string): string { + return toNamespacedPath(resolve(path)); +} + +function getQuarantineRoot(packageDir: string): string | undefined { + let current = resolve(packageDir); + while (true) { + if (basename(current).toLowerCase() === "node_modules") { + return join(current, QUARANTINE_DIR_NAME); + } + const parent = dirname(current); + if (parent === current) { + return undefined; + } + current = parent; + } +} + +function getLoadedSharedObjectsInPackageDir(packageDir: string): string[] { + const sharedObjects = (process.report.getReport() as { sharedObjects?: unknown }).sharedObjects; + if (!Array.isArray(sharedObjects)) { + return []; + } + + const root = normalizePath(packageDir).toLowerCase(); + const seen = new Set(); + const loadedFiles: string[] = []; + for (const value of sharedObjects) { + if (typeof value !== "string") { + continue; + } + const filePath = normalizePath(value); + const comparisonPath = filePath.toLowerCase(); + if (getCwdRelativePath(comparisonPath, root) === undefined || seen.has(comparisonPath)) { + continue; + } + seen.add(comparisonPath); + loadedFiles.push(filePath); + } + return loadedFiles; +} + +export function cleanupWindowsSelfUpdateQuarantine(packageDir: string): void { + const quarantineRoot = getQuarantineRoot(packageDir); + if (!quarantineRoot) { + return; + } + try { + rmSync(quarantineRoot, { recursive: true, force: true }); + } catch { + // A previous pi process may still be exiting and holding a native addon. + } +} + +export function quarantineWindowsNativeDependencies(packageDir: string): void { + const resolvedPackageDir = normalizePath(packageDir); + const quarantineRoot = getQuarantineRoot(resolvedPackageDir); + if (!quarantineRoot) { + return; + } + + const loadedFiles = getLoadedSharedObjectsInPackageDir(resolvedPackageDir); + if (loadedFiles.length === 0) { + return; + } + + const quarantineRunDir = join(quarantineRoot, `${Date.now()}-${process.pid}-${randomUUID()}`); + for (const loadedFile of loadedFiles) { + if (!existsSync(loadedFile)) { + continue; + } + const quarantinePath = join(quarantineRunDir, relative(resolvedPackageDir, loadedFile)); + mkdirSync(dirname(quarantinePath), { recursive: true }); + renameSync(loadedFile, quarantinePath); + copyFileSync(quarantinePath, loadedFile); + } +}