Skip to content

Commit ec37863

Browse files
authored
Merge pull request #203 from drivecore/fix-cli-hanging-issue
Fix: CLI hanging issue - implement resource cleanup
2 parents 3d3e76b + d33e729 commit ec37863

File tree

4 files changed

+117
-6
lines changed

4 files changed

+117
-6
lines changed

packages/agent/src/tools/browser/BrowserManager.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ export class BrowserManager {
1414
headless: true,
1515
defaultTimeout: 30000,
1616
};
17+
18+
constructor() {
19+
// Store a reference to the instance globally for cleanup
20+
// This allows the CLI to access the instance for cleanup
21+
(globalThis as any).__BROWSER_MANAGER__ = this;
22+
23+
// Set up cleanup handlers for graceful shutdown
24+
this.setupGlobalCleanup();
25+
}
1726

1827
async createSession(config?: BrowserConfig): Promise<BrowserSession> {
1928
try {
@@ -80,14 +89,41 @@ export class BrowserManager {
8089
this.sessions.delete(session.id);
8190
});
8291

83-
// Handle process exit
92+
// No need to add individual process handlers for each session
93+
// We'll handle all sessions in the global cleanup
94+
}
95+
96+
/**
97+
* Sets up global cleanup handlers for all browser sessions
98+
*/
99+
private setupGlobalCleanup(): void {
100+
// Use beforeExit for async cleanup
101+
process.on('beforeExit', () => {
102+
this.closeAllSessions().catch((err) => {
103+
console.error('Error closing browser sessions:', err);
104+
});
105+
});
106+
107+
// Use exit for synchronous cleanup (as a fallback)
84108
process.on('exit', () => {
85-
this.closeSession(session.id).catch(() => {});
109+
// Can only do synchronous operations here
110+
for (const session of this.sessions.values()) {
111+
try {
112+
// Attempt synchronous close - may not fully work
113+
session.browser.close();
114+
} catch (e) {
115+
// Ignore errors during exit
116+
}
117+
}
86118
});
87-
88-
// Handle unexpected errors
89-
process.on('uncaughtException', () => {
90-
this.closeSession(session.id).catch(() => {});
119+
120+
// Handle SIGINT (Ctrl+C)
121+
process.on('SIGINT', () => {
122+
this.closeAllSessions().catch(() => {})
123+
.finally(() => {
124+
// Give a moment for cleanup to complete
125+
setTimeout(() => process.exit(0), 500);
126+
});
91127
});
92128
}
93129

packages/agent/src/tools/system/shellStart.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type ProcessState = {
2525
};
2626

2727
// Global map to store process state
28+
// This is exported so it can be accessed for cleanup
2829
export const processStates: Map<string, ProcessState> = new Map();
2930

3031
const parameterSchema = z.object({

packages/cli/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { command as toolsCommand } from './commands/tools.js';
1313
import { sharedOptions } from './options.js';
1414
import { initSentry, captureException } from './sentry/index.js';
1515
import { getConfig } from './settings/config.js';
16+
import { cleanupResources, setupForceExit } from './utils/cleanup.js';
1617
import { enableProfiling, mark, reportTimings } from './utils/performance.js';
1718

1819
mark('After imports');
@@ -82,4 +83,11 @@ await main()
8283
.finally(async () => {
8384
// Report timings if profiling is enabled
8485
await reportTimings();
86+
87+
// Clean up all resources before exit
88+
await cleanupResources();
89+
90+
// Setup a force exit as a failsafe
91+
// This ensures the process will exit even if there are lingering handles
92+
setupForceExit(5000);
8593
});

packages/cli/src/utils/cleanup.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { BrowserManager, processStates } from 'mycoder-agent';
2+
3+
/**
4+
* Handles cleanup of resources before application exit
5+
* Ensures all browser sessions and shell processes are terminated
6+
*/
7+
export async function cleanupResources(): Promise<void> {
8+
console.log('Cleaning up resources before exit...');
9+
10+
// 1. Clean up browser sessions
11+
try {
12+
// Get the BrowserManager instance - this is a singleton
13+
const browserManager = (globalThis as any).__BROWSER_MANAGER__ as BrowserManager | undefined;
14+
if (browserManager) {
15+
console.log('Closing all browser sessions...');
16+
await browserManager.closeAllSessions();
17+
}
18+
} catch (error) {
19+
console.error('Error closing browser sessions:', error);
20+
}
21+
22+
// 2. Clean up shell processes
23+
try {
24+
if (processStates.size > 0) {
25+
console.log(`Terminating ${processStates.size} shell processes...`);
26+
for (const [id, state] of processStates.entries()) {
27+
if (!state.state.completed) {
28+
console.log(`Terminating process ${id}...`);
29+
try {
30+
state.process.kill('SIGTERM');
31+
// Force kill after a short timeout if still running
32+
setTimeout(() => {
33+
try {
34+
if (!state.state.completed) {
35+
state.process.kill('SIGKILL');
36+
}
37+
} catch (e) {
38+
// Ignore errors on forced kill
39+
}
40+
}, 500);
41+
} catch (e) {
42+
console.error(`Error terminating process ${id}:`, e);
43+
}
44+
}
45+
}
46+
}
47+
} catch (error) {
48+
console.error('Error terminating shell processes:', error);
49+
}
50+
51+
// 3. Give async operations a moment to complete
52+
await new Promise((resolve) => setTimeout(resolve, 1000));
53+
54+
console.log('Cleanup completed');
55+
}
56+
57+
/**
58+
* Force exits the process after a timeout
59+
* This is a failsafe to ensure the process exits even if there are lingering handles
60+
*/
61+
export function setupForceExit(timeoutMs = 5000): void {
62+
setTimeout(() => {
63+
console.log(`Forcing exit after ${timeoutMs}ms timeout`);
64+
process.exit(0);
65+
}, timeoutMs);
66+
}

0 commit comments

Comments
 (0)