Skip to content

Commit 27259c3

Browse files
Merge pull request #212 from apify/fix/devtools-server-eaddrinuse
fix: prevent DevToolsServer port conflicts on BrowserPool relaunch Fixes a Web Scraper dev-mode issue where DevToolsServer was started from BrowserPool’s preLaunchHook on every browser launch/relaunch, causing EADDRINUSE port conflicts after browser/page crashes or pool relaunches. The PR makes DevToolsServer startup idempotent by starting it only once per actor process (reusing a cached start promise on subsequent hook calls) and registers a single Actor.on('exit') cleanup to stop the server.
2 parents 9c08287 + f88b276 commit 27259c3

2 files changed

Lines changed: 85 additions & 13 deletions

File tree

packages/actor-scraper/web-scraper/src/internals/crawler_setup.ts

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,37 @@ export class CrawlerSetup implements CrawlerSetupOptions {
225225
this.initPromise = this._initializeAsync();
226226
}
227227

228+
private static devToolsStartPromise: Promise<DevToolsServer | null> | null =
229+
null;
230+
231+
private static async getDevToolsServer(): Promise<DevToolsServer | null> {
232+
if (this.devToolsStartPromise) return this.devToolsStartPromise;
233+
234+
this.devToolsStartPromise = (async () => {
235+
const url = process.env.ACTOR_WEB_SERVER_URL;
236+
const portRaw = process.env.ACTOR_WEB_SERVER_PORT;
237+
if (!url || !portRaw) return null;
238+
239+
const port = Number(portRaw);
240+
if (!Number.isFinite(port)) return null;
241+
242+
const server = new DevToolsServer({
243+
containerHost: new URL(url).host,
244+
devToolsServerPort: port,
245+
chromeRemoteDebuggingPort: CHROME_DEBUGGER_PORT,
246+
});
247+
248+
await server.start();
249+
250+
Actor.on('exit', () => {
251+
server.stop?.();
252+
});
253+
return server;
254+
})();
255+
256+
return this.devToolsStartPromise;
257+
}
258+
228259
private async _initializeAsync() {
229260
// RequestList
230261
const startUrls = this.input.startUrls.map((req) => {
@@ -311,19 +342,8 @@ export class CrawlerSetup implements CrawlerSetupOptions {
311342
browserPoolOptions: {
312343
preLaunchHooks: [
313344
async () => {
314-
if (!this.isDevRun) {
315-
return;
316-
}
317-
318-
const devToolsServer = new DevToolsServer({
319-
containerHost: new URL(
320-
process.env.ACTOR_WEB_SERVER_URL!,
321-
).host,
322-
devToolsServerPort:
323-
process.env.ACTOR_WEB_SERVER_PORT,
324-
chromeRemoteDebuggingPort: CHROME_DEBUGGER_PORT,
325-
});
326-
await devToolsServer.start();
345+
if (!this.isDevRun) return;
346+
await CrawlerSetup.getDevToolsServer();
327347
},
328348
],
329349
},
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
3+
describe('DevToolsServer (regression)', () => {
4+
it('starts only once even if preLaunchHook calls it multiple times', async () => {
5+
const oldEnv = process.env;
6+
const actorOnMock = vi.fn();
7+
8+
const startMock = vi.fn(async () => {});
9+
const stopMock = vi.fn(() => {});
10+
const DevToolsCtorMock = vi.fn(() => ({
11+
start: startMock,
12+
stop: stopMock,
13+
}));
14+
15+
try {
16+
process.env = { ...oldEnv };
17+
process.env.ACTOR_WEB_SERVER_URL = 'http://127.0.0.1:4321';
18+
process.env.ACTOR_WEB_SERVER_PORT = '4321';
19+
20+
vi.resetModules();
21+
vi.doMock('devtools-server', () => ({ default: DevToolsCtorMock }));
22+
vi.doMock('apify', () => ({ Actor: { on: actorOnMock } }));
23+
24+
const mod = await import(
25+
new URL('../src/internals/crawler_setup.ts', import.meta.url)
26+
.href
27+
);
28+
const { CrawlerSetup } = mod as any;
29+
30+
CrawlerSetup.devToolsServerPromise = null;
31+
32+
const fn =
33+
CrawlerSetup.getDevToolsServer ??
34+
CrawlerSetup.startDevToolsServerOnce;
35+
36+
await Promise.all(
37+
Array.from({ length: 10 }, () => fn.call(CrawlerSetup)),
38+
);
39+
40+
expect(DevToolsCtorMock).toHaveBeenCalledTimes(1);
41+
expect(startMock).toHaveBeenCalledTimes(1);
42+
expect(actorOnMock).toHaveBeenCalledTimes(1);
43+
expect(actorOnMock).toHaveBeenCalledWith(
44+
'exit',
45+
expect.any(Function),
46+
);
47+
} finally {
48+
process.env = oldEnv;
49+
vi.resetModules();
50+
}
51+
});
52+
});

0 commit comments

Comments
 (0)