Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .codex/environments/environment.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "EnsoAI"

[setup]
script = ""

[[actions]]
name = "运行"
icon = "run"
command = "pnpm dev"
7 changes: 7 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,13 @@ app.on('will-quit', (event) => {

const forceExitTimer = setTimeout(() => {
console.error('[app] Cleanup timed out, forcing exit');
// Best-effort: kill native resources synchronously before forcing exit to
// avoid deadlocking during Node addon cleanup (e.g. node-pty on macOS).
try {
cleanupAllResourcesSync();
} catch (err) {
console.error('[app] Sync cleanup error:', err);
}
app.exit(0);
}, FORCE_EXIT_TIMEOUT_MS);

Expand Down
25 changes: 16 additions & 9 deletions src/main/ipc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { stopAllCodeReviews } from '../services/ai';
import { disposeClaudeIdeBridge } from '../services/claude/ClaudeIdeBridge';
import { autoUpdaterService } from '../services/updater/AutoUpdater';
import { webInspectorServer } from '../services/webInspector';
import { cleanupExecInPtys, cleanupExecInPtysSync } from '../utils/shell';
import { registerAgentHandlers } from './agent';
import { registerAppHandlers } from './app';
import { registerClaudeConfigHandlers } from './claudeConfig';
Expand All @@ -16,12 +17,7 @@ import {
stopAllFileWatchersSync,
} from './files';
import { clearAllGitServices, registerGitHandlers } from './git';
import {
autoStartHapi,
cleanupHapi,
cleanupHapiSync,
registerHapiHandlers,
} from './hapi';
import { autoStartHapi, cleanupHapi, cleanupHapiSync, registerHapiHandlers } from './hapi';

export { autoStartHapi };

Expand All @@ -36,7 +32,7 @@ import {
destroyAllTerminalsAndWait,
registerTerminalHandlers,
} from './terminal';
import { cleanupTmux, cleanupTmuxSync, registerTmuxHandlers } from './tmux';
import { cleanupTmuxSync, registerTmuxHandlers } from './tmux';
import { cleanupTodo, registerTodoHandlers } from './todo';
import { registerUpdaterHandlers } from './updater';
import { registerWebInspectorHandlers } from './webInspector';
Expand Down Expand Up @@ -69,11 +65,19 @@ export function registerIpcHandlers(): void {
export async function cleanupAllResources(): Promise<void> {
const CLEANUP_TIMEOUT = 3000;

// Ensure any in-flight execInPty commands are terminated before Node shutdown.
// Leaving node-pty PTYs alive can deadlock native addon cleanup on macOS.
await cleanupExecInPtys(CLEANUP_TIMEOUT);

// Stop Hapi server first (graceful best-effort with timeout)
await cleanupHapi(CLEANUP_TIMEOUT);

// Kill tmux enso server (async, fast)
cleanupTmux().catch((err) => console.warn('Tmux cleanup warning:', err));
// Kill tmux enso server (sync, best-effort). Avoid spawning new PTYs during shutdown.
try {
cleanupTmuxSync();
} catch (err) {
console.warn('Tmux cleanup warning:', err);
}

// Stop Web Inspector server (sync, fast)
webInspectorServer.stop();
Expand Down Expand Up @@ -132,6 +136,9 @@ export async function cleanupAllResources(): Promise<void> {
export function cleanupAllResourcesSync(): void {
console.log('[app] Sync cleanup starting...');

// Kill any in-flight execInPty commands first (sync)
cleanupExecInPtysSync();

// Kill Hapi/Cloudflared processes (sync)
cleanupHapiSync();

Expand Down
51 changes: 46 additions & 5 deletions src/main/services/terminal/PtyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ interface PtySession {
cwd: string;
onData: (data: string) => void;
onExit?: (exitCode: number, signal?: number) => void;
// node-pty returns disposables for event subscriptions; keep them so we can
// dispose during shutdown and avoid native threads hanging during Node cleanup.
dataDisposable: { dispose(): void };
exitDisposable?: { dispose(): void };
}

function findFallbackShell(): string {
Expand Down Expand Up @@ -407,21 +411,36 @@ export class PtyManager {
}
}

ptyProcess.onData((data) => {
const dataDisposable = ptyProcess.onData((data) => {
onData(data);
});

// Store session first so onExit callback can access it
const session: PtySession = { pty: ptyProcess, cwd, onData, onExit };
const session: PtySession = { pty: ptyProcess, cwd, onData, onExit, dataDisposable };
this.sessions.set(id, session);

ptyProcess.onExit(({ exitCode, signal }) => {
const exitDisposable = ptyProcess.onExit(({ exitCode, signal }) => {
// Read onExit from session to allow it to be replaced during cleanup
const currentSession = this.sessions.get(id);
const exitHandler = currentSession?.onExit;

// Dispose subscriptions promptly to release native resources (node-pty TSFN)
try {
currentSession?.dataDisposable.dispose();
} catch {
// Ignore
}
try {
currentSession?.exitDisposable?.dispose();
} catch {
// Ignore
}

this.sessions.delete(id);
this.activityCache.delete(id);
exitHandler?.(exitCode, signal);
});
session.exitDisposable = exitDisposable;

return id;
}
Expand All @@ -443,6 +462,17 @@ export class PtyManager {
destroy(id: string): void {
const session = this.sessions.get(id);
if (session) {
// Stop delivering data/exit callbacks immediately.
try {
session.dataDisposable.dispose();
} catch {
// Ignore
}
try {
session.exitDisposable?.dispose();
} catch {
// Ignore
}
killProcessTree(session.pty);
this.sessions.delete(id);
this.activityCache.delete(id);
Expand All @@ -461,14 +491,26 @@ export class PtyManager {
return;
}

const _pid = session.pty.pid;
let resolved = false;

// Stop data callbacks during shutdown; we only care about exit.
try {
session.dataDisposable.dispose();
} catch {
// Ignore
}

// Set up timeout
const timer = setTimeout(() => {
if (!resolved) {
resolved = true;
try {
session.exitDisposable?.dispose();
} catch {
// Ignore
}
this.sessions.delete(id);
this.activityCache.delete(id);
resolve();
}
}, timeout);
Expand All @@ -478,7 +520,6 @@ export class PtyManager {
if (!resolved) {
resolved = true;
clearTimeout(timer);
this.sessions.delete(id);
// Don't call original onExit during cleanup to avoid issues
resolve();
}
Expand Down
30 changes: 26 additions & 4 deletions src/main/utils/processUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,21 @@ export function killProcessTree(
} catch {
// pgrep may not exist or fail, continue to kill main process
}
// Kill the main process
// Kill the main process.
// Prefer target.kill() when available so libraries (e.g. node-pty) can run
// their own cleanup logic around termination.
try {
process.kill(pid, signal);
if (typeof target !== 'number' && 'kill' in target) {
try {
target.kill(signal);
} catch {
// Fallback for ProcessLike implementations that don't support the signal
// or throw while process.kill would still work.
process.kill(pid, signal);
}
} else {
process.kill(pid, signal);
}
} catch {
// Ignore - process may have already exited
}
Expand Down Expand Up @@ -128,9 +140,19 @@ export async function killProcessTreeAsync(
}
}

// Kill the main process
// Kill the main process.
// Prefer target.kill() when available so libraries (e.g. node-pty) can run
// their own cleanup logic around termination.
try {
process.kill(pid, signal);
if (typeof target !== 'number' && 'kill' in target) {
try {
target.kill(signal);
} catch {
process.kill(pid, signal);
}
} else {
process.kill(pid, signal);
}
} catch {
// Ignore
}
Expand Down
Loading
Loading