improvement

This commit is contained in:
iliya 2026-02-27 15:37:37 +02:00
parent 17e20847d6
commit 427c5478e1
5 changed files with 79 additions and 12 deletions

View file

@ -28,7 +28,7 @@
</p>
<p align="center">
<sub>100% free, open source. No API keys. No configuration. Just download, open, and see everything Claude Code did.</sub>
<sub>100% free, open source. No API keys. No configuration.</sub>
</p>
<br />

View file

@ -58,6 +58,12 @@ const INSTALL_TIMEOUT_MS = 120_000;
/** Max redirects to follow when fetching from GCS */
const MAX_REDIRECTS = 5;
/** Max retries for EBUSY (antivirus scanning the new binary) */
const EBUSY_MAX_RETRIES = 3;
/** Delay between EBUSY retries (multiplied by attempt number) */
const EBUSY_RETRY_DELAY_MS = 2000;
// =============================================================================
// Helpers
// =============================================================================
@ -331,6 +337,12 @@ export class CliInstallerService {
await fsp.chmod(tmpFilePath, 0o755);
}
// On Windows, antivirus (Defender) scans new executables on first access.
// A brief pause lets the scan complete before we spawn, preventing EBUSY.
if (process.platform === 'win32') {
await new Promise((r) => setTimeout(r, 1000));
}
this.sendProgress({
type: 'installing',
detail: 'Starting shell integration...',
@ -411,7 +423,11 @@ export class CliInstallerService {
});
res.on('end', () => {
fileStream.end(() => resolve(hash.digest('hex')));
const digest = hash.digest('hex');
fileStream.end();
// Wait for 'close' (not just 'finish') — ensures file descriptor is fully released.
// On Windows, spawning the file before 'close' can cause EBUSY.
fileStream.on('close', () => resolve(digest));
});
res.on('error', (err) => {
@ -429,8 +445,9 @@ export class CliInstallerService {
/**
* Run `claude install` via spawn with streaming output.
* Collects all output for error context. Non-zero exit tolerated if binary resolves.
* Retries on EBUSY (antivirus scanning the new binary).
*/
private async runInstallWithStreaming(binaryPath: string): Promise<void> {
private async runInstallWithStreaming(binaryPath: string, attempt = 1): Promise<void> {
return new Promise<void>((resolve, reject) => {
const child = spawn(binaryPath, ['install'], {
env: { ...process.env, CLAUDE_SKIP_ANALYTICS: '1' },
@ -491,6 +508,24 @@ export class CliInstallerService {
child.on('error', (err) => {
clearTimeout(timeout);
// EBUSY: antivirus (Windows Defender / macOS Gatekeeper) may be scanning the binary — retry
const isEbusy = (err as NodeJS.ErrnoException).code === 'EBUSY';
if (isEbusy && attempt < EBUSY_MAX_RETRIES) {
const delayMs = attempt * EBUSY_RETRY_DELAY_MS;
logger.warn(
`spawn EBUSY (attempt ${attempt}/${EBUSY_MAX_RETRIES}), retrying in ${delayMs}ms...`
);
this.sendProgress({
type: 'installing',
rawChunk: `\r\n⏳ File busy (OS scan), retrying in ${delayMs / 1000}s...\r\n`,
});
setTimeout(() => {
this.runInstallWithStreaming(binaryPath, attempt + 1).then(resolve, reject);
}, delayMs);
return;
}
reject(err);
});
});

View file

@ -6,8 +6,8 @@
*/
import crypto from 'node:crypto';
import os from 'node:os';
import { getHomeDir } from '@main/utils/pathDecoder';
// eslint-disable-next-line boundaries/element-types -- IPC channel constants shared between main and preload
import { TERMINAL_DATA, TERMINAL_EXIT } from '@preload/constants/ipcChannels';
import { createLogger } from '@shared/utils/logger';
@ -62,7 +62,7 @@ export class PtyTerminalService {
name: 'xterm-256color',
cols: options?.cols ?? 80,
rows: options?.rows ?? 24,
cwd: options?.cwd ?? os.homedir(),
cwd: options?.cwd ?? getHomeDir(),
env: { ...process.env, ...options?.env } as Record<string, string>,
});

View file

@ -223,9 +223,42 @@ export function buildTodoPath(claudeBasePath: string, sessionId: string): string
// =============================================================================
/**
* Get the user's home directory.
* Try Electron's app.getPath('home') which correctly handles Unicode paths
* on Windows (Cyrillic, CJK, etc.) unlike Node's os.homedir() / env vars
* that can suffer from UTF-8 vs system codepage mismatches.
*
* Returns null when Electron app is unavailable (e.g. in tests).
*/
function getHomeDir(): string {
function getElectronHome(): string | null {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports -- Lazy require to avoid hard dependency on electron in test environments
const electron = require('electron') as {
app?: { getPath: (name: string) => string };
};
const app = electron.app;
if (app && typeof app.getPath === 'function') {
const home = app.getPath('home');
if (home) return home;
}
} catch {
// Not in Electron context (tests, standalone builds, etc.)
}
return null;
}
/**
* Get the user's home directory.
*
* Priority:
* 1. Electron app.getPath('home') correct Unicode handling on all platforms
* 2. HOME env var (Unix) / USERPROFILE (Windows)
* 3. HOMEDRIVE + HOMEPATH (Windows fallback)
* 4. os.homedir() (Node.js built-in)
*/
export function getHomeDir(): string {
const electronHome = getElectronHome();
if (electronHome) return electronHome;
const windowsHome =
process.env.HOMEDRIVE && process.env.HOMEPATH
? `${process.env.HOMEDRIVE}${process.env.HOMEPATH}`

View file

@ -6,10 +6,9 @@
*/
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { getClaudeBasePath } from './pathDecoder';
import { getClaudeBasePath, getHomeDir } from './pathDecoder';
/**
* Sensitive file patterns that should never be accessible.
@ -149,7 +148,7 @@ export function validateFilePath(
// Expand ~ to home directory
const expandedPath = filePath.startsWith('~')
? path.join(os.homedir(), filePath.slice(1))
? path.join(getHomeDir(), filePath.slice(1))
: filePath;
// Must be absolute path
@ -212,7 +211,7 @@ export function validateOpenPathUserSelected(targetPath: string): PathValidation
}
const expandedPath = targetPath.startsWith('~')
? path.join(os.homedir(), targetPath.slice(1))
? path.join(getHomeDir(), targetPath.slice(1))
: targetPath;
const normalizedPath = path.resolve(path.normalize(expandedPath));
@ -256,7 +255,7 @@ export function validateOpenPath(
// Expand ~ to home directory
const expandedPath = targetPath.startsWith('~')
? path.join(os.homedir(), targetPath.slice(1))
? path.join(getHomeDir(), targetPath.slice(1))
: targetPath;
const normalizedPath = path.resolve(path.normalize(expandedPath));