improvement
This commit is contained in:
parent
17e20847d6
commit
427c5478e1
5 changed files with 79 additions and 12 deletions
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
Loading…
Reference in a new issue