From 0099be4f237223a883f9875a720354bcb34f1e47 Mon Sep 17 00:00:00 2001 From: iliya Date: Mon, 2 Mar 2026 23:10:44 +0200 Subject: [PATCH] fix: improve WSL user home path resolution and enhance process renaming on Windows - Updated fallback home path logic to correctly handle 'root' user case in WSL. - Refactored process renaming in TeamAgentToolsInstaller to include retry logic on Windows for better error handling during concurrent operations. - Enhanced user information retrieval in TeamProvisioningService to handle potential errors gracefully. - Clarified documentation in processKill utility regarding platform-specific behavior. --- src/main/ipc/config.ts | 6 +++++- .../services/team/ClaudeBinaryResolver.ts | 5 ++--- .../services/team/TeamAgentToolsInstaller.ts | 21 ++++++++++++++++++- .../services/team/TeamProvisioningService.ts | 9 +++++++- src/main/utils/processKill.ts | 3 ++- 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/main/ipc/config.ts b/src/main/ipc/config.ts index 79b13a69..838ad650 100644 --- a/src/main/ipc/config.ts +++ b/src/main/ipc/config.ts @@ -981,7 +981,11 @@ async function handleFindWslClaudeRoots( // Fallback: query the default WSL user, then try Windows USERNAME const wslUser = await resolveWslDefaultUser(distro); const fallbackUser = wslUser || process.env.USERNAME; - const fallbackHomePath = fallbackUser ? `/home/${fallbackUser}` : null; + const fallbackHomePath = fallbackUser + ? fallbackUser === 'root' + ? '/root' + : `/home/${fallbackUser}` + : null; const normalizedHome = normalizeWslHomePath(resolvedHomePath ?? '') ?? (fallbackHomePath ? normalizeWslHomePath(fallbackHomePath) : null); diff --git a/src/main/services/team/ClaudeBinaryResolver.ts b/src/main/services/team/ClaudeBinaryResolver.ts index 6df3888f..ba9f28e3 100644 --- a/src/main/services/team/ClaudeBinaryResolver.ts +++ b/src/main/services/team/ClaudeBinaryResolver.ts @@ -97,9 +97,8 @@ async function collectNvmWindowsCandidates(): Promise { const exts = getWindowsExecutableExtensions(); return versions - .flatMap((version) => exts.map((ext) => path.join(nvmRoot, version, `claude${ext}`))) - .sort((a, b) => a.localeCompare(b)) - .reverse(); + .sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: 'base' })) + .flatMap((version) => exts.map((ext) => path.join(nvmRoot, version, `claude${ext}`))); } async function resolveFromPathEnv(binaryName: string): Promise { diff --git a/src/main/services/team/TeamAgentToolsInstaller.ts b/src/main/services/team/TeamAgentToolsInstaller.ts index 504c7e19..6cd215c9 100644 --- a/src/main/services/team/TeamAgentToolsInstaller.ts +++ b/src/main/services/team/TeamAgentToolsInstaller.ts @@ -153,7 +153,26 @@ function atomicWrite(filePath, data) { '.' + String(Math.random().toString(16).slice(2)); fs.writeFileSync(tmp, data, 'utf8'); - fs.renameSync(tmp, filePath); + // On Windows, fs.renameSync can throw EPERM/EACCES when another process + // is concurrently renaming to the same target. Retry with backoff. + const maxRetries = process.platform === 'win32' ? 5 : 1; + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + fs.renameSync(tmp, filePath); + return; + } catch (e) { + if (attempt < maxRetries - 1 && e && (e.code === 'EPERM' || e.code === 'EACCES')) { + // Busy wait — small random delay to reduce contention + const ms = Math.floor(Math.random() * 50) + 10; + const end = Date.now() + ms; + while (Date.now() < end) { /* spin */ } + continue; + } + // Clean up temp file on final failure + try { fs.unlinkSync(tmp); } catch { /* ignore */ } + throw e; + } + } } function normalizeStatus(value) { diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 8c53fed3..6acba5f9 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -2702,11 +2702,18 @@ export class TeamProvisioningService { const electronHome = getHomeDir(); const isWindows = process.platform === 'win32'; const home = shellEnv.HOME?.trim() || electronHome; + let osUsername = ''; + try { + osUsername = os.userInfo().username; + } catch { + // os.userInfo() can throw SystemError in restricted environments (no passwd entry, Docker, etc.) + } const user = shellEnv.USER?.trim() || process.env.USER?.trim() || process.env.USERNAME?.trim() || - os.userInfo().username; + osUsername || + 'unknown'; // Shell: on Windows there is no SHELL env var; use COMSPEC (cmd.exe / powershell). // On Unix, prefer the user's login shell from env or fall back to /bin/zsh. diff --git a/src/main/utils/processKill.ts b/src/main/utils/processKill.ts index 84976248..fdcf4fcd 100644 --- a/src/main/utils/processKill.ts +++ b/src/main/utils/processKill.ts @@ -10,7 +10,8 @@ import path from 'path'; * it calls TerminateProcess() which is equivalent to SIGKILL (immediate, ungraceful). * - `taskkill /T` also kills child processes, preventing orphaned process trees. * - * Throws if the process cannot be killed (except ESRCH — process already dead). + * On Unix, throws if the process cannot be killed (except ESRCH — process already dead). + * On Windows, taskkill is best-effort (async fire-and-forget) to match killProcessTree() semantics. */ export function killProcessByPid(pid: number): void { if (process.platform === 'win32') {