Skip to content

Commit 73384d4

Browse files
committed
refactor(agent): merge SessionManager functionality into SessionTracker
This commit refactors the SessionTracker class to include the functionality from SessionManager. It adds methods for creating, managing, and closing browser sessions, which were previously handled by SessionManager. This reduces code duplication and simplifies the codebase. Resolves #311
1 parent f037f14 commit 73384d4

File tree

1 file changed

+302
-0
lines changed

1 file changed

+302
-0
lines changed
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
import { chromium } from '@playwright/test';
2+
import { v4 as uuidv4 } from 'uuid';
3+
4+
import {
5+
browserSessions,
6+
BrowserConfig,
7+
Session,
8+
BrowserError,
9+
BrowserErrorCode,
10+
} from './lib/types.js';
11+
12+
// Status of a browser session
13+
export enum SessionStatus {
14+
RUNNING = 'running',
15+
COMPLETED = 'completed',
16+
ERROR = 'error',
17+
TERMINATED = 'terminated',
18+
}
19+
20+
export interface SessionInfo {
21+
id: string;
22+
status: SessionStatus;
23+
startTime: Date;
24+
endTime?: Date;
25+
metadata: {
26+
url?: string;
27+
contentLength?: number;
28+
closedExplicitly?: boolean;
29+
error?: string;
30+
actionType?: string;
31+
closedByCleanup?: boolean;
32+
};
33+
}
34+
35+
/**
36+
* Registry to keep track of browser sessions
37+
*/
38+
export class SessionTracker {
39+
private sessions: Map<string, SessionInfo> = new Map();
40+
private readonly defaultConfig: BrowserConfig = {
41+
headless: true,
42+
defaultTimeout: 30000,
43+
};
44+
45+
constructor(public ownerAgentId: string | undefined) {
46+
// Store a reference to the instance globally for cleanup
47+
// This allows the CLI to access the instance for cleanup
48+
(globalThis as Record<string, unknown>).__BROWSER_MANAGER__ = this;
49+
50+
// Set up cleanup handlers for graceful shutdown
51+
this.setupGlobalCleanup();
52+
}
53+
54+
// Register a new browser session
55+
registerBrowser(url?: string): string {
56+
const id = uuidv4();
57+
this.sessions.set(id, {
58+
id,
59+
status: SessionStatus.RUNNING,
60+
startTime: new Date(),
61+
metadata: {
62+
url: url || 'about:blank',
63+
},
64+
});
65+
return id;
66+
}
67+
68+
// Update the status of a session
69+
updateSessionStatus(
70+
id: string,
71+
status: SessionStatus,
72+
metadata?: Record<string, unknown>,
73+
): boolean {
74+
const session = this.sessions.get(id);
75+
if (!session) return false;
76+
77+
session.status = status;
78+
79+
// If the session is no longer running, set the end time
80+
if (status !== SessionStatus.RUNNING) {
81+
session.endTime = new Date();
82+
}
83+
84+
// Update metadata if provided
85+
if (metadata) {
86+
session.metadata = { ...session.metadata, ...metadata };
87+
}
88+
89+
return true;
90+
}
91+
92+
// Get all sessions
93+
getSessions(): SessionInfo[] {
94+
return Array.from(this.sessions.values());
95+
}
96+
97+
// Get a session by ID
98+
getSessionById(id: string): SessionInfo | undefined {
99+
return this.sessions.get(id);
100+
}
101+
102+
// Get sessions by status
103+
getSessionsByStatus(status: SessionStatus): SessionInfo[] {
104+
return this.getSessions().filter((session) => session.status === status);
105+
}
106+
107+
/**
108+
* Creates a new browser session
109+
* @param config Optional browser configuration
110+
* @returns A promise that resolves to a browser session
111+
*/
112+
async createSession(config?: BrowserConfig): Promise<Session> {
113+
try {
114+
const sessionConfig = { ...this.defaultConfig, ...config };
115+
const browser = await chromium.launch({
116+
headless: sessionConfig.headless,
117+
});
118+
119+
// Create a new context (equivalent to Puppeteer's incognito context)
120+
const context = await browser.newContext({
121+
viewport: null,
122+
userAgent:
123+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
124+
});
125+
126+
const page = await context.newPage();
127+
page.setDefaultTimeout(sessionConfig.defaultTimeout ?? 30000);
128+
129+
const id = this.registerBrowser();
130+
131+
const session: Session = {
132+
browser,
133+
page,
134+
id,
135+
};
136+
137+
browserSessions.set(id, session);
138+
this.setupCleanup(session);
139+
140+
return session;
141+
} catch (error) {
142+
throw new BrowserError(
143+
'Failed to create browser session',
144+
BrowserErrorCode.LAUNCH_FAILED,
145+
error,
146+
);
147+
}
148+
}
149+
150+
/**
151+
* Closes a browser session by ID
152+
* @param sessionId The ID of the session to close
153+
*/
154+
async closeSession(sessionId: string): Promise<void> {
155+
const session = browserSessions.get(sessionId);
156+
if (!session) {
157+
throw new BrowserError(
158+
'Session not found',
159+
BrowserErrorCode.SESSION_ERROR,
160+
);
161+
}
162+
163+
try {
164+
// In Playwright, we should close the context which will automatically close its pages
165+
await session.page.context().close();
166+
await session.browser.close();
167+
browserSessions.delete(sessionId);
168+
169+
// Update session status
170+
this.updateSessionStatus(sessionId, SessionStatus.COMPLETED, {
171+
closedExplicitly: true,
172+
});
173+
} catch (error) {
174+
throw new BrowserError(
175+
'Failed to close session',
176+
BrowserErrorCode.SESSION_ERROR,
177+
error,
178+
);
179+
}
180+
}
181+
182+
/**
183+
* Gets a browser session by ID
184+
* @param sessionId The ID of the session to get
185+
* @returns The browser session
186+
*/
187+
getSession(sessionId: string): Session {
188+
const session = browserSessions.get(sessionId);
189+
if (!session) {
190+
throw new BrowserError(
191+
'Session not found',
192+
BrowserErrorCode.SESSION_ERROR,
193+
);
194+
}
195+
return session;
196+
}
197+
198+
/**
199+
* Cleans up all browser sessions associated with this tracker
200+
* @returns A promise that resolves when cleanup is complete
201+
*/
202+
async cleanup(): Promise<void> {
203+
const cleanupPromises = Array.from(this.sessions.values()).map((session) =>
204+
this.cleanupSession(session),
205+
);
206+
await Promise.all(cleanupPromises);
207+
}
208+
209+
/**
210+
* Cleans up a browser session
211+
* @param session The browser session to clean up
212+
*/
213+
private async cleanupSession(session: SessionInfo): Promise<void> {
214+
// Only clean up running sessions
215+
if (session.status !== SessionStatus.RUNNING) return;
216+
217+
const browserSession = browserSessions.get(session.id);
218+
if (!browserSession) return;
219+
220+
try {
221+
// Close the browser session
222+
await browserSession.page.context().close();
223+
await browserSession.browser.close();
224+
browserSessions.delete(session.id);
225+
226+
// Update session status
227+
this.updateSessionStatus(session.id, SessionStatus.TERMINATED, {
228+
closedByCleanup: true,
229+
});
230+
} catch {
231+
// Ignore errors during cleanup
232+
}
233+
}
234+
235+
/**
236+
* Closes all browser sessions
237+
* @returns A promise that resolves when all sessions are closed
238+
*/
239+
async closeAllSessions(): Promise<void> {
240+
const closePromises = Array.from(this.sessions.keys())
241+
.filter(
242+
(sessionId) =>
243+
this.sessions.get(sessionId)?.status === SessionStatus.RUNNING,
244+
)
245+
.map((sessionId) => this.closeSession(sessionId).catch(() => {}));
246+
await Promise.all(closePromises);
247+
}
248+
249+
/**
250+
* Sets up cleanup handlers for a browser session
251+
* @param session The browser session to set up cleanup handlers for
252+
*/
253+
private setupCleanup(session: Session): void {
254+
// Handle browser disconnection
255+
session.browser.on('disconnected', () => {
256+
browserSessions.delete(session.id);
257+
this.updateSessionStatus(session.id, SessionStatus.TERMINATED);
258+
});
259+
}
260+
261+
/**
262+
* Sets up global cleanup handlers for all browser sessions
263+
*/
264+
private setupGlobalCleanup(): void {
265+
// Use beforeExit for async cleanup
266+
process.on('beforeExit', () => {
267+
this.closeAllSessions().catch((err) => {
268+
console.error('Error closing browser sessions:', err);
269+
});
270+
});
271+
272+
// Use exit for synchronous cleanup (as a fallback)
273+
process.on('exit', () => {
274+
// Can only do synchronous operations here
275+
for (const sessionId of browserSessions.keys()) {
276+
try {
277+
const session = browserSessions.get(sessionId);
278+
if (session) {
279+
// Attempt synchronous close - may not fully work
280+
session.browser.close();
281+
}
282+
// eslint-disable-next-line unused-imports/no-unused-vars
283+
} catch (e) {
284+
// Ignore errors during exit
285+
}
286+
}
287+
});
288+
289+
// Handle SIGINT (Ctrl+C)
290+
process.on('SIGINT', () => {
291+
// eslint-disable-next-line promise/catch-or-return
292+
this.closeAllSessions()
293+
.catch(() => {
294+
return false;
295+
})
296+
.finally(() => {
297+
// Give a moment for cleanup to complete
298+
setTimeout(() => process.exit(0), 500);
299+
});
300+
});
301+
}
302+
}

0 commit comments

Comments
 (0)