deckide 3.5.36 → 3.5.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import fsSync from 'node:fs';
|
|
4
|
-
import net from 'node:net';
|
|
5
4
|
import os from 'node:os';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { WebSocket } from 'ws';
|
|
@@ -23,6 +22,22 @@ function sleep(ms) {
|
|
|
23
22
|
function isOpen(socket) {
|
|
24
23
|
return socket.readyState === WebSocket.OPEN;
|
|
25
24
|
}
|
|
25
|
+
function waitForSocketOpen(socket, timeoutMs = 10_000) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
reject(new Error('Timed out connecting to Chrome DevTools WebSocket'));
|
|
29
|
+
}, timeoutMs);
|
|
30
|
+
timer.unref?.();
|
|
31
|
+
socket.once('open', () => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
resolve();
|
|
34
|
+
});
|
|
35
|
+
socket.once('error', (error) => {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
26
41
|
function coercePositiveInt(value, fallback, min, max) {
|
|
27
42
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
28
43
|
return fallback;
|
|
@@ -105,35 +120,57 @@ async function findChromeExecutable() {
|
|
|
105
120
|
}
|
|
106
121
|
throw new Error(`AGENT_BROWSER_CHROME does not exist: ${process.env.AGENT_BROWSER_CHROME}`);
|
|
107
122
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
return findInPath([
|
|
123
|
+
// 本物の Google Chrome を最優先(指紋が自然でボット判定を通りやすい)。
|
|
124
|
+
// Playwright の Chromium はフォールバックに回す。
|
|
125
|
+
const systemChrome = await findInPath([
|
|
113
126
|
'google-chrome',
|
|
114
127
|
'google-chrome-stable',
|
|
115
128
|
'chromium',
|
|
116
129
|
'chromium-browser',
|
|
117
130
|
'chrome',
|
|
118
131
|
]);
|
|
132
|
+
if (systemChrome) {
|
|
133
|
+
return systemChrome;
|
|
134
|
+
}
|
|
135
|
+
return findPlaywrightChromium();
|
|
119
136
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
// MCP(chrome-devtools-mcp 等)が --browserUrl で確実に相乗りできるよう、
|
|
138
|
+
// CDP ポートは固定にする。AGENT_BROWSER_CDP_PORT で上書き可、既定 9222。
|
|
139
|
+
const DEFAULT_CDP_PORT = 9222;
|
|
140
|
+
function resolveCdpPort() {
|
|
141
|
+
const raw = process.env.AGENT_BROWSER_CDP_PORT;
|
|
142
|
+
const parsed = raw ? Number.parseInt(raw, 10) : NaN;
|
|
143
|
+
if (Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535) {
|
|
144
|
+
return parsed;
|
|
145
|
+
}
|
|
146
|
+
return DEFAULT_CDP_PORT;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Chrome の起動コマンドを解決する。
|
|
150
|
+
* ボット判定対策として「実 Chrome を headful」で動かしたい。Linux サーバ等で
|
|
151
|
+
* ディスプレイが無い場合は xvfb-run で仮想ディスプレイ上の headful を使う。
|
|
152
|
+
* xvfb-run も無ければやむを得ず headless にフォールバックする。
|
|
153
|
+
*/
|
|
154
|
+
function resolveLaunchCommand(executable, chromeArgs) {
|
|
155
|
+
const hasDisplay = Boolean(process.env.DISPLAY);
|
|
156
|
+
if (process.platform === 'linux' && !hasDisplay) {
|
|
157
|
+
const xvfbRun = ['/usr/bin/xvfb-run', '/usr/local/bin/xvfb-run'].find((p) => fsSync.existsSync(p));
|
|
158
|
+
if (xvfbRun) {
|
|
159
|
+
return {
|
|
160
|
+
command: xvfbRun,
|
|
161
|
+
args: [
|
|
162
|
+
'-a',
|
|
163
|
+
'--server-args=-screen 0 1280x720x24 -nolisten tcp',
|
|
164
|
+
executable,
|
|
165
|
+
...chromeArgs,
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// ディスプレイも xvfb-run も無い → headless で動かす(Cloudflare は通りにくい)。
|
|
170
|
+
return { command: executable, args: ['--headless=new', ...chromeArgs] };
|
|
171
|
+
}
|
|
172
|
+
// ディスプレイがある(mac/win/Linux+X)→ そのまま headful 起動。
|
|
173
|
+
return { command: executable, args: chromeArgs };
|
|
137
174
|
}
|
|
138
175
|
async function fetchJson(url, options = {}, timeoutMs = 5000) {
|
|
139
176
|
const controller = new AbortController();
|
|
@@ -243,10 +280,27 @@ export class AgentBrowserService {
|
|
|
243
280
|
launching = null;
|
|
244
281
|
port = null;
|
|
245
282
|
executablePath = null;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
283
|
+
// Browser-level CDP connection used only to discover tabs (page targets) and
|
|
284
|
+
// drive their lifecycle (create/close/activate). One per running Chrome.
|
|
285
|
+
browserCdp = null;
|
|
286
|
+
browserCdpSocket = null;
|
|
287
|
+
browserCdpConnecting = null;
|
|
288
|
+
// CDP connection to the *active* tab's page target — the only tab that streams
|
|
289
|
+
// frames and receives input. Switching tabs tears this down and reconnects to
|
|
290
|
+
// the new target.
|
|
291
|
+
activeCdp = null;
|
|
292
|
+
activeCdpSocket = null;
|
|
293
|
+
activeConnecting = null;
|
|
294
|
+
// Bumped on every tab switch. A slower in-flight connect compares against it
|
|
295
|
+
// and bails if it lost the race, so it can't clobber the newer active tab.
|
|
296
|
+
activeGeneration = 0;
|
|
249
297
|
screencastStarted = false;
|
|
298
|
+
// Known page targets (tabs) keyed by targetId, in discovery/creation order.
|
|
299
|
+
tabs = new Map();
|
|
300
|
+
activeTargetId = null;
|
|
301
|
+
// True once the first active tab is established. Gates popup auto-follow so
|
|
302
|
+
// targets discovered during launch don't hijack focus before we've picked one.
|
|
303
|
+
ready = false;
|
|
250
304
|
// Raw JPEG bytes of the most recent screencast frame, kept so reconnecting or
|
|
251
305
|
// late-joining clients can be shown the current page immediately. Frames are
|
|
252
306
|
// pushed to clients as binary WebSocket messages (no base64) to cut ~33% size
|
|
@@ -257,13 +311,12 @@ export class AgentBrowserService {
|
|
|
257
311
|
frameFlushTimer = null;
|
|
258
312
|
clients = new Set();
|
|
259
313
|
viewport = { ...DEFAULT_VIEWPORT };
|
|
260
|
-
pageUrl = null;
|
|
261
|
-
pageTitle = null;
|
|
262
314
|
lastError;
|
|
263
315
|
audioRelay = new BrowserAudioRelay();
|
|
264
316
|
profileDir = process.env.AGENT_BROWSER_PROFILE_DIR || DEFAULT_PROFILE_DIR;
|
|
265
317
|
outputDir = process.env.AGENT_BROWSER_OUTPUT_DIR || DEFAULT_OUTPUT_DIR;
|
|
266
318
|
async getStatus() {
|
|
319
|
+
const active = this.activeTargetId ? this.tabs.get(this.activeTargetId) : null;
|
|
267
320
|
return {
|
|
268
321
|
running: this.isRunning(),
|
|
269
322
|
launching: Boolean(this.launching),
|
|
@@ -271,15 +324,17 @@ export class AgentBrowserService {
|
|
|
271
324
|
outputDir: this.outputDir,
|
|
272
325
|
executablePath: this.executablePath ?? await findChromeExecutable().catch(() => null),
|
|
273
326
|
cdpUrl: this.port ? `http://127.0.0.1:${this.port}` : null,
|
|
274
|
-
pageUrl:
|
|
275
|
-
pageTitle:
|
|
327
|
+
pageUrl: active?.url ?? null,
|
|
328
|
+
pageTitle: active?.title ?? null,
|
|
329
|
+
tabs: [...this.tabs.values()].map((tab) => ({ ...tab })),
|
|
330
|
+
activeTabId: this.activeTargetId,
|
|
276
331
|
audio: await this.audioRelay.getStatus(),
|
|
277
332
|
error: this.lastError,
|
|
278
333
|
};
|
|
279
334
|
}
|
|
280
335
|
async start() {
|
|
281
336
|
if (this.isRunning()) {
|
|
282
|
-
await this.
|
|
337
|
+
await this.ensureSession();
|
|
283
338
|
if (this.clients.size > 0) {
|
|
284
339
|
await this.startScreencast();
|
|
285
340
|
}
|
|
@@ -309,12 +364,14 @@ export class AgentBrowserService {
|
|
|
309
364
|
}
|
|
310
365
|
}
|
|
311
366
|
async stop() {
|
|
312
|
-
this.
|
|
367
|
+
this.disconnectActiveCdp();
|
|
368
|
+
this.closeBrowserCdp();
|
|
313
369
|
const proc = this.chromeProcess;
|
|
314
370
|
this.chromeProcess = null;
|
|
315
371
|
this.port = null;
|
|
316
|
-
this.
|
|
317
|
-
this.
|
|
372
|
+
this.tabs.clear();
|
|
373
|
+
this.activeTargetId = null;
|
|
374
|
+
this.ready = false;
|
|
318
375
|
this.screencastStarted = false;
|
|
319
376
|
this.lastFrameBuffer = null;
|
|
320
377
|
this.pendingFrames.clear();
|
|
@@ -372,12 +429,62 @@ export class AgentBrowserService {
|
|
|
372
429
|
async navigate(input) {
|
|
373
430
|
const url = normalizeBrowserUrl(input);
|
|
374
431
|
await this.start();
|
|
375
|
-
await this.
|
|
376
|
-
await this.
|
|
377
|
-
this.
|
|
378
|
-
|
|
432
|
+
await this.ensureActiveCdp();
|
|
433
|
+
await this.activeCdp?.send('Page.navigate', { url });
|
|
434
|
+
const tab = this.activeTargetId ? this.tabs.get(this.activeTargetId) : null;
|
|
435
|
+
if (tab) {
|
|
436
|
+
tab.url = url;
|
|
437
|
+
}
|
|
438
|
+
await this.refreshActivePageInfo();
|
|
439
|
+
await this.broadcastStatus();
|
|
440
|
+
}
|
|
441
|
+
// 履歴を delta だけ移動(-1=戻る, +1=進む)。端なら何もしない。
|
|
442
|
+
async goHistory(delta) {
|
|
443
|
+
await this.start();
|
|
444
|
+
await this.ensureActiveCdp();
|
|
445
|
+
const cdp = this.activeCdp;
|
|
446
|
+
if (!cdp)
|
|
447
|
+
return;
|
|
448
|
+
const history = await cdp.send('Page.getNavigationHistory');
|
|
449
|
+
const targetIndex = history.currentIndex + delta;
|
|
450
|
+
if (targetIndex < 0 || targetIndex >= history.entries.length) {
|
|
451
|
+
return; // これ以上戻る/進む先がない
|
|
452
|
+
}
|
|
453
|
+
await cdp.send('Page.navigateToHistoryEntry', { entryId: history.entries[targetIndex].id });
|
|
454
|
+
await this.refreshActivePageInfo();
|
|
379
455
|
await this.broadcastStatus();
|
|
380
456
|
}
|
|
457
|
+
async newTab(input) {
|
|
458
|
+
await this.start();
|
|
459
|
+
await this.connectBrowserCdp();
|
|
460
|
+
const url = input ? normalizeBrowserUrl(input) : 'about:blank';
|
|
461
|
+
const result = await this.browserCdp?.send('Target.createTarget', { url });
|
|
462
|
+
const targetId = result?.targetId;
|
|
463
|
+
if (!targetId) {
|
|
464
|
+
throw new Error('Failed to open a new tab');
|
|
465
|
+
}
|
|
466
|
+
if (!this.tabs.has(targetId)) {
|
|
467
|
+
this.tabs.set(targetId, { id: targetId, url: url === 'about:blank' ? '' : url, title: '' });
|
|
468
|
+
}
|
|
469
|
+
await this.setActiveTab(targetId);
|
|
470
|
+
}
|
|
471
|
+
async closeTab(targetId) {
|
|
472
|
+
await this.connectBrowserCdp();
|
|
473
|
+
await this.browserCdp?.send('Target.closeTarget', { targetId }).catch(() => undefined);
|
|
474
|
+
this.removeTab(targetId);
|
|
475
|
+
// Never leave the pane with zero tabs — reopen a blank one like a real
|
|
476
|
+
// browser keeping a minimum of one tab.
|
|
477
|
+
if (this.tabs.size === 0 && this.isRunning()) {
|
|
478
|
+
await this.newTab().catch(() => undefined);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async activateTab(targetId) {
|
|
482
|
+
if (!this.tabs.has(targetId)) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
await this.setActiveTab(targetId);
|
|
486
|
+
await this.browserCdp?.send('Target.activateTarget', { targetId }).catch(() => undefined);
|
|
487
|
+
}
|
|
381
488
|
async launch() {
|
|
382
489
|
this.lastError = undefined;
|
|
383
490
|
await fs.mkdir(this.profileDir, { recursive: true });
|
|
@@ -387,10 +494,11 @@ export class AgentBrowserService {
|
|
|
387
494
|
throw new Error('Chromium or Chrome was not found. Set AGENT_BROWSER_CHROME to a browser executable.');
|
|
388
495
|
}
|
|
389
496
|
this.executablePath = executable;
|
|
390
|
-
this.port =
|
|
497
|
+
this.port = resolveCdpPort();
|
|
391
498
|
this.openLogStream();
|
|
392
|
-
|
|
393
|
-
|
|
499
|
+
// headless フラグは付けない(headful を既定にする)。ディスプレイ無し環境では
|
|
500
|
+
// resolveLaunchCommand が xvfb-run で仮想ディスプレイを用意する。
|
|
501
|
+
const chromeArgs = [
|
|
394
502
|
`--remote-debugging-address=127.0.0.1`,
|
|
395
503
|
`--remote-debugging-port=${this.port}`,
|
|
396
504
|
`--user-data-dir=${this.profileDir}`,
|
|
@@ -398,13 +506,23 @@ export class AgentBrowserService {
|
|
|
398
506
|
'--no-default-browser-check',
|
|
399
507
|
'--disable-dev-shm-usage',
|
|
400
508
|
'--autoplay-policy=no-user-gesture-required',
|
|
509
|
+
// GPU の無いサーバ + Xvfb 上で headful 起動すると、別プロセスの GPU が
|
|
510
|
+
// 起動失敗して "GPU process isn't usable. Goodbye." で即クラッシュする。
|
|
511
|
+
// GPU を browser プロセス内で動かし、GPU サンドボックスを外して回避する。
|
|
512
|
+
'--in-process-gpu',
|
|
513
|
+
'--disable-gpu-sandbox',
|
|
514
|
+
// ボット判定(Cloudflare の Managed Challenge 等)で誤検知されにくくする。
|
|
515
|
+
// navigator.webdriver を立てる自動化フラグを無効化する。残りの偽装は CDP 側。
|
|
516
|
+
'--disable-blink-features=AutomationControlled',
|
|
517
|
+
'--lang=ja-JP',
|
|
401
518
|
'--window-size=1280,720',
|
|
402
|
-
'about:blank',
|
|
403
519
|
];
|
|
404
520
|
if (process.getuid?.() === 0) {
|
|
405
|
-
|
|
521
|
+
chromeArgs.push('--no-sandbox');
|
|
406
522
|
}
|
|
407
|
-
|
|
523
|
+
chromeArgs.push('about:blank');
|
|
524
|
+
const { command, args } = resolveLaunchCommand(executable, chromeArgs);
|
|
525
|
+
const child = spawn(command, args, {
|
|
408
526
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
409
527
|
env: process.env,
|
|
410
528
|
});
|
|
@@ -415,7 +533,11 @@ export class AgentBrowserService {
|
|
|
415
533
|
if (this.chromeProcess === child) {
|
|
416
534
|
this.chromeProcess = null;
|
|
417
535
|
this.port = null;
|
|
418
|
-
this.
|
|
536
|
+
this.disconnectActiveCdp();
|
|
537
|
+
this.closeBrowserCdp();
|
|
538
|
+
this.tabs.clear();
|
|
539
|
+
this.activeTargetId = null;
|
|
540
|
+
this.ready = false;
|
|
419
541
|
this.closeLogStream();
|
|
420
542
|
this.lastError = code === 0 ? undefined : `Browser exited (${signal ?? code ?? 'unknown'})`;
|
|
421
543
|
void this.broadcastStatus();
|
|
@@ -426,8 +548,8 @@ export class AgentBrowserService {
|
|
|
426
548
|
void this.broadcastStatus();
|
|
427
549
|
});
|
|
428
550
|
await this.waitForChrome();
|
|
429
|
-
await this.
|
|
430
|
-
await this.
|
|
551
|
+
await this.connectBrowserCdp();
|
|
552
|
+
await this.ensureActiveTab();
|
|
431
553
|
await this.broadcastStatus();
|
|
432
554
|
}
|
|
433
555
|
async waitForChrome() {
|
|
@@ -455,7 +577,7 @@ export class AgentBrowserService {
|
|
|
455
577
|
async openForClient(socket) {
|
|
456
578
|
try {
|
|
457
579
|
await this.start();
|
|
458
|
-
await this.
|
|
580
|
+
await this.ensureActiveCdp();
|
|
459
581
|
await this.sendStatus(socket);
|
|
460
582
|
// Replay the most recent frame so a reconnecting or late-joining client
|
|
461
583
|
// sees the current page immediately. Screencast only emits frames on
|
|
@@ -479,80 +601,258 @@ export class AgentBrowserService {
|
|
|
479
601
|
const proc = this.chromeProcess;
|
|
480
602
|
return Boolean(proc && proc.exitCode == null && proc.signalCode == null);
|
|
481
603
|
}
|
|
482
|
-
|
|
483
|
-
|
|
604
|
+
// Make sure the browser-level connection and an active tab (+ its CDP) exist.
|
|
605
|
+
async ensureSession() {
|
|
606
|
+
await this.connectBrowserCdp();
|
|
607
|
+
await this.ensureActiveTab();
|
|
608
|
+
}
|
|
609
|
+
async connectBrowserCdp() {
|
|
610
|
+
if (this.browserCdp && this.browserCdpSocket && isOpen(this.browserCdpSocket)) {
|
|
484
611
|
return;
|
|
485
612
|
}
|
|
486
|
-
if (this.
|
|
487
|
-
return this.
|
|
613
|
+
if (this.browserCdpConnecting) {
|
|
614
|
+
return this.browserCdpConnecting;
|
|
488
615
|
}
|
|
489
|
-
this.
|
|
616
|
+
this.browserCdpConnecting = this.doConnectBrowserCdp();
|
|
490
617
|
try {
|
|
491
|
-
await this.
|
|
618
|
+
await this.browserCdpConnecting;
|
|
492
619
|
}
|
|
493
620
|
finally {
|
|
494
|
-
this.
|
|
621
|
+
this.browserCdpConnecting = null;
|
|
495
622
|
}
|
|
496
623
|
}
|
|
497
|
-
async
|
|
624
|
+
async doConnectBrowserCdp() {
|
|
498
625
|
if (!this.port) {
|
|
499
626
|
throw new Error('Browser is not running');
|
|
500
627
|
}
|
|
501
|
-
const
|
|
502
|
-
if (!
|
|
503
|
-
throw new Error('Chrome
|
|
628
|
+
const version = await fetchJson(`http://127.0.0.1:${this.port}/json/version`);
|
|
629
|
+
if (!version.webSocketDebuggerUrl) {
|
|
630
|
+
throw new Error('Chrome has no browser-level DevTools endpoint');
|
|
504
631
|
}
|
|
505
|
-
this.
|
|
506
|
-
const socket = new WebSocket(
|
|
507
|
-
await
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
reject(error);
|
|
518
|
-
});
|
|
632
|
+
this.closeBrowserCdp();
|
|
633
|
+
const socket = new WebSocket(version.webSocketDebuggerUrl);
|
|
634
|
+
await waitForSocketOpen(socket);
|
|
635
|
+
const cdp = new CdpConnection(socket);
|
|
636
|
+
cdp.onEvent = (message) => this.handleBrowserEvent(message);
|
|
637
|
+
this.browserCdp = cdp;
|
|
638
|
+
this.browserCdpSocket = socket;
|
|
639
|
+
socket.on('close', () => {
|
|
640
|
+
if (this.browserCdpSocket === socket) {
|
|
641
|
+
this.browserCdp = null;
|
|
642
|
+
this.browserCdpSocket = null;
|
|
643
|
+
}
|
|
519
644
|
});
|
|
645
|
+
// Emits Target.targetCreated for every existing target, then keeps us posted
|
|
646
|
+
// on creation/url/title changes and destruction.
|
|
647
|
+
await cdp.send('Target.setDiscoverTargets', { discover: true });
|
|
648
|
+
}
|
|
649
|
+
async ensureActiveTab() {
|
|
650
|
+
await this.connectBrowserCdp();
|
|
651
|
+
// Discovery events normally populate `tabs`; if they haven't landed yet (or
|
|
652
|
+
// none exist), fall back to the HTTP target list, then create one.
|
|
653
|
+
if (this.tabs.size === 0) {
|
|
654
|
+
const targets = await fetchJson(`http://127.0.0.1:${this.port}/json/list`)
|
|
655
|
+
.catch(() => []);
|
|
656
|
+
for (const target of targets) {
|
|
657
|
+
if (target.type === 'page' && !target.url.startsWith('devtools://')) {
|
|
658
|
+
this.tabs.set(target.id, { id: target.id, url: target.url, title: target.title ?? '' });
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (this.tabs.size === 0) {
|
|
663
|
+
const created = await this.browserCdp?.send('Target.createTarget', {
|
|
664
|
+
url: 'about:blank',
|
|
665
|
+
});
|
|
666
|
+
const targetId = created?.targetId;
|
|
667
|
+
if (!targetId) {
|
|
668
|
+
throw new Error('Failed to open a browser tab');
|
|
669
|
+
}
|
|
670
|
+
this.tabs.set(targetId, { id: targetId, url: '', title: '' });
|
|
671
|
+
await this.setActiveTab(targetId);
|
|
672
|
+
this.ready = true;
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (!this.activeTargetId || !this.tabs.has(this.activeTargetId)) {
|
|
676
|
+
const first = this.tabs.keys().next().value;
|
|
677
|
+
await this.setActiveTab(first);
|
|
678
|
+
}
|
|
679
|
+
else if (!this.activeCdp || !this.activeCdpSocket || !isOpen(this.activeCdpSocket)) {
|
|
680
|
+
await this.connectActiveCdp(this.activeTargetId);
|
|
681
|
+
}
|
|
682
|
+
this.ready = true;
|
|
683
|
+
}
|
|
684
|
+
async ensureActiveCdp() {
|
|
685
|
+
if (this.activeCdp && this.activeCdpSocket && isOpen(this.activeCdpSocket)) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (this.activeConnecting) {
|
|
689
|
+
return this.activeConnecting;
|
|
690
|
+
}
|
|
691
|
+
this.activeConnecting = (async () => {
|
|
692
|
+
if (!this.activeTargetId || !this.tabs.has(this.activeTargetId)) {
|
|
693
|
+
await this.ensureActiveTab();
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
await this.connectActiveCdp(this.activeTargetId);
|
|
697
|
+
}
|
|
698
|
+
})();
|
|
699
|
+
try {
|
|
700
|
+
await this.activeConnecting;
|
|
701
|
+
}
|
|
702
|
+
finally {
|
|
703
|
+
this.activeConnecting = null;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
async setActiveTab(targetId) {
|
|
707
|
+
this.activeGeneration += 1;
|
|
708
|
+
const generation = this.activeGeneration;
|
|
709
|
+
this.activeTargetId = targetId;
|
|
710
|
+
this.clearActiveFrame();
|
|
711
|
+
// Drop the stale page from every client while the new tab's first frame is
|
|
712
|
+
// still in flight.
|
|
713
|
+
this.broadcast({ type: 'clear' });
|
|
714
|
+
try {
|
|
715
|
+
await this.connectActiveCdp(targetId, generation);
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
if (generation === this.activeGeneration) {
|
|
719
|
+
this.lastError = error instanceof Error ? error.message : String(error);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
await this.broadcastStatus();
|
|
723
|
+
}
|
|
724
|
+
async connectActiveCdp(targetId, generation = this.activeGeneration) {
|
|
725
|
+
if (!this.port) {
|
|
726
|
+
throw new Error('Browser is not running');
|
|
727
|
+
}
|
|
728
|
+
this.disconnectActiveCdp();
|
|
729
|
+
const socket = new WebSocket(`ws://127.0.0.1:${this.port}/devtools/page/${targetId}`);
|
|
730
|
+
try {
|
|
731
|
+
await waitForSocketOpen(socket);
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
try {
|
|
735
|
+
socket.close();
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// ignore
|
|
739
|
+
}
|
|
740
|
+
throw error;
|
|
741
|
+
}
|
|
742
|
+
// A newer switch started while we were connecting — discard this one.
|
|
743
|
+
if (generation !== this.activeGeneration) {
|
|
744
|
+
try {
|
|
745
|
+
socket.close();
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
// ignore
|
|
749
|
+
}
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
520
752
|
const cdp = new CdpConnection(socket);
|
|
521
|
-
cdp.onEvent = (message) => this.
|
|
522
|
-
this.
|
|
523
|
-
this.
|
|
753
|
+
cdp.onEvent = (message) => this.handleActiveEvent(message);
|
|
754
|
+
this.activeCdp = cdp;
|
|
755
|
+
this.activeCdpSocket = socket;
|
|
524
756
|
this.screencastStarted = false;
|
|
525
|
-
this.pageUrl = target.url || this.pageUrl;
|
|
526
|
-
this.pageTitle = target.title || this.pageTitle;
|
|
527
757
|
socket.on('close', () => {
|
|
528
|
-
if (this.
|
|
529
|
-
this.
|
|
530
|
-
this.
|
|
758
|
+
if (this.activeCdpSocket === socket) {
|
|
759
|
+
this.activeCdp = null;
|
|
760
|
+
this.activeCdpSocket = null;
|
|
531
761
|
this.screencastStarted = false;
|
|
532
762
|
}
|
|
533
763
|
});
|
|
534
764
|
await cdp.send('Page.enable');
|
|
535
765
|
await cdp.send('Runtime.enable');
|
|
766
|
+
await this.applyAntiDetection(cdp);
|
|
767
|
+
if (generation !== this.activeGeneration) {
|
|
768
|
+
this.disconnectActiveCdp();
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
536
771
|
await this.applyViewport();
|
|
772
|
+
if (this.clients.size > 0) {
|
|
773
|
+
await this.startScreencast();
|
|
774
|
+
}
|
|
537
775
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
776
|
+
/**
|
|
777
|
+
* ヘッドレス Chrome がボット判定(Cloudflare の Managed Challenge / Turnstile 等)で
|
|
778
|
+
* 誤検知されるのを減らす。完全な突破は保証できないが、代表的な自動化シグナルを消す:
|
|
779
|
+
* - UA から "HeadlessChrome" を除去(実バージョンに一致させる)
|
|
780
|
+
* - navigator.webdriver / languages / chrome / plugins / WebGL ベンダを通常ブラウザ相当に
|
|
781
|
+
* 偽装スクリプトは addScriptToEvaluateOnNewDocument で「以後の」ページ生成時に毎回適用される。
|
|
782
|
+
*/
|
|
783
|
+
async applyAntiDetection(cdp) {
|
|
784
|
+
try {
|
|
785
|
+
// 実際のブラウザ版に一致した UA を使う(バージョン不一致自体が検知シグナルになるため)。
|
|
786
|
+
// ページセッションでは Browser.* が使えないことがあるので /json/version を優先。
|
|
787
|
+
let userAgent = '';
|
|
788
|
+
try {
|
|
789
|
+
const info = await fetchJson(`http://127.0.0.1:${this.port}/json/version`);
|
|
790
|
+
userAgent = (info['User-Agent'] || '').replace(/HeadlessChrome/gi, 'Chrome');
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
// フォールバック: ページの navigator.userAgent を取得して整形。
|
|
794
|
+
const r = await cdp.send('Runtime.evaluate', {
|
|
795
|
+
expression: 'navigator.userAgent',
|
|
796
|
+
returnByValue: true,
|
|
797
|
+
});
|
|
798
|
+
userAgent = String(r.result?.value ?? '').replace(/HeadlessChrome/gi, 'Chrome');
|
|
799
|
+
}
|
|
800
|
+
const major = (userAgent.match(/Chrome\/(\d+)/) || [])[1] || '120';
|
|
801
|
+
if (userAgent) {
|
|
802
|
+
await cdp.send('Emulation.setUserAgentOverride', {
|
|
803
|
+
userAgent,
|
|
804
|
+
acceptLanguage: 'ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
805
|
+
platform: 'Linux x86_64',
|
|
806
|
+
userAgentMetadata: {
|
|
807
|
+
brands: [
|
|
808
|
+
{ brand: 'Not_A Brand', version: '24' },
|
|
809
|
+
{ brand: 'Chromium', version: major },
|
|
810
|
+
{ brand: 'Google Chrome', version: major },
|
|
811
|
+
],
|
|
812
|
+
fullVersion: (userAgent.match(/Chrome\/([\d.]+)/) || [])[1] || `${major}.0.0.0`,
|
|
813
|
+
platform: 'Linux',
|
|
814
|
+
platformVersion: '',
|
|
815
|
+
architecture: 'x86',
|
|
816
|
+
model: '',
|
|
817
|
+
mobile: false,
|
|
818
|
+
},
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
const stealthSource = `
|
|
822
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
823
|
+
if (!window.chrome) { window.chrome = { runtime: {} }; }
|
|
824
|
+
Object.defineProperty(navigator, 'languages', { get: () => ['ja-JP', 'ja', 'en-US', 'en'] });
|
|
825
|
+
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
|
|
826
|
+
try {
|
|
827
|
+
const _q = window.navigator.permissions && window.navigator.permissions.query;
|
|
828
|
+
if (_q) {
|
|
829
|
+
window.navigator.permissions.query = (p) =>
|
|
830
|
+
p && p.name === 'notifications'
|
|
831
|
+
? Promise.resolve({ state: Notification.permission })
|
|
832
|
+
: _q(p);
|
|
833
|
+
}
|
|
834
|
+
} catch (e) { /* ignore */ }
|
|
835
|
+
try {
|
|
836
|
+
const getParam = WebGLRenderingContext.prototype.getParameter;
|
|
837
|
+
WebGLRenderingContext.prototype.getParameter = function (p) {
|
|
838
|
+
if (p === 37445) return 'Intel Inc.';
|
|
839
|
+
if (p === 37446) return 'Intel Iris OpenGL Engine';
|
|
840
|
+
return getParam.call(this, p);
|
|
841
|
+
};
|
|
842
|
+
} catch (e) { /* ignore */ }
|
|
843
|
+
`;
|
|
844
|
+
await cdp.send('Page.addScriptToEvaluateOnNewDocument', { source: stealthSource });
|
|
541
845
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const page = targets.find((target) => target.type === 'page' && target.webSocketDebuggerUrl);
|
|
545
|
-
if (page) {
|
|
546
|
-
return page;
|
|
846
|
+
catch {
|
|
847
|
+
// 反検知設定は best-effort。失敗してもブラウジング自体は続行する。
|
|
547
848
|
}
|
|
548
|
-
return fetchJson(`${baseUrl}/json/new?about:blank`, { method: 'PUT' });
|
|
549
849
|
}
|
|
550
850
|
async startScreencast() {
|
|
551
|
-
if (this.screencastStarted || !this.
|
|
851
|
+
if (this.screencastStarted || !this.activeCdp) {
|
|
552
852
|
return;
|
|
553
853
|
}
|
|
554
854
|
await this.applyViewport();
|
|
555
|
-
await this.
|
|
855
|
+
await this.activeCdp.send('Page.startScreencast', {
|
|
556
856
|
format: 'jpeg',
|
|
557
857
|
quality: 72,
|
|
558
858
|
maxWidth: this.viewport.width,
|
|
@@ -562,11 +862,11 @@ export class AgentBrowserService {
|
|
|
562
862
|
this.screencastStarted = true;
|
|
563
863
|
}
|
|
564
864
|
async stopScreencast() {
|
|
565
|
-
if (!this.screencastStarted || !this.
|
|
865
|
+
if (!this.screencastStarted || !this.activeCdp) {
|
|
566
866
|
return;
|
|
567
867
|
}
|
|
568
868
|
try {
|
|
569
|
-
await this.
|
|
869
|
+
await this.activeCdp.send('Page.stopScreencast');
|
|
570
870
|
}
|
|
571
871
|
catch {
|
|
572
872
|
// ignore
|
|
@@ -576,10 +876,10 @@ export class AgentBrowserService {
|
|
|
576
876
|
}
|
|
577
877
|
}
|
|
578
878
|
async applyViewport() {
|
|
579
|
-
if (!this.
|
|
879
|
+
if (!this.activeCdp) {
|
|
580
880
|
return;
|
|
581
881
|
}
|
|
582
|
-
await this.
|
|
882
|
+
await this.activeCdp.send('Emulation.setDeviceMetricsOverride', {
|
|
583
883
|
width: this.viewport.width,
|
|
584
884
|
height: this.viewport.height,
|
|
585
885
|
deviceScaleFactor: 1,
|
|
@@ -597,12 +897,12 @@ export class AgentBrowserService {
|
|
|
597
897
|
return;
|
|
598
898
|
}
|
|
599
899
|
this.viewport = next;
|
|
600
|
-
if (!this.
|
|
900
|
+
if (!this.activeCdp) {
|
|
601
901
|
return;
|
|
602
902
|
}
|
|
603
903
|
await this.applyViewport();
|
|
604
904
|
if (this.screencastStarted) {
|
|
605
|
-
await this.
|
|
905
|
+
await this.activeCdp.send('Page.stopScreencast').catch(() => undefined);
|
|
606
906
|
this.screencastStarted = false;
|
|
607
907
|
await this.startScreencast();
|
|
608
908
|
}
|
|
@@ -628,11 +928,37 @@ export class AgentBrowserService {
|
|
|
628
928
|
return;
|
|
629
929
|
}
|
|
630
930
|
await this.start();
|
|
631
|
-
|
|
931
|
+
if (message.type === 'new-tab') {
|
|
932
|
+
await this.newTab(message.url);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (message.type === 'close-tab') {
|
|
936
|
+
await this.closeTab(message.targetId);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (message.type === 'activate-tab') {
|
|
940
|
+
await this.activateTab(message.targetId);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
await this.ensureActiveCdp();
|
|
632
944
|
if (message.type === 'navigate') {
|
|
633
945
|
await this.navigate(message.url);
|
|
634
946
|
return;
|
|
635
947
|
}
|
|
948
|
+
if (message.type === 'back') {
|
|
949
|
+
await this.goHistory(-1);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (message.type === 'forward') {
|
|
953
|
+
await this.goHistory(1);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (message.type === 'reload') {
|
|
957
|
+
await this.activeCdp?.send('Page.reload', {});
|
|
958
|
+
await this.refreshActivePageInfo();
|
|
959
|
+
await this.broadcastStatus();
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
636
962
|
if (message.type === 'mouse') {
|
|
637
963
|
await this.dispatchMouse(message);
|
|
638
964
|
return;
|
|
@@ -649,9 +975,9 @@ export class AgentBrowserService {
|
|
|
649
975
|
}
|
|
650
976
|
}
|
|
651
977
|
async dispatchMouse(message) {
|
|
652
|
-
if (!this.
|
|
978
|
+
if (!this.activeCdp)
|
|
653
979
|
return;
|
|
654
|
-
await this.
|
|
980
|
+
await this.activeCdp.send('Input.dispatchMouseEvent', {
|
|
655
981
|
type: message.eventType,
|
|
656
982
|
x: coercePositiveInt(message.x, 0, 0, MAX_VIEWPORT.width),
|
|
657
983
|
y: coercePositiveInt(message.y, 0, 0, MAX_VIEWPORT.height),
|
|
@@ -663,10 +989,10 @@ export class AgentBrowserService {
|
|
|
663
989
|
});
|
|
664
990
|
}
|
|
665
991
|
async dispatchKey(message) {
|
|
666
|
-
if (!this.
|
|
992
|
+
if (!this.activeCdp)
|
|
667
993
|
return;
|
|
668
994
|
const windowsVirtualKeyCode = getWindowsVirtualKeyCode(message.key);
|
|
669
|
-
await this.
|
|
995
|
+
await this.activeCdp.send('Input.dispatchKeyEvent', {
|
|
670
996
|
type: message.eventType,
|
|
671
997
|
key: message.key,
|
|
672
998
|
code: message.code,
|
|
@@ -677,13 +1003,14 @@ export class AgentBrowserService {
|
|
|
677
1003
|
modifiers: message.modifiers ?? 0,
|
|
678
1004
|
});
|
|
679
1005
|
}
|
|
680
|
-
|
|
1006
|
+
// Events from the active tab's page CDP connection.
|
|
1007
|
+
handleActiveEvent(message) {
|
|
681
1008
|
if (message.method === 'Page.screencastFrame') {
|
|
682
1009
|
const params = message.params;
|
|
683
1010
|
const data = typeof params.data === 'string' ? params.data : null;
|
|
684
1011
|
const sessionId = typeof params.sessionId === 'number' ? params.sessionId : null;
|
|
685
1012
|
if (sessionId != null) {
|
|
686
|
-
void this.
|
|
1013
|
+
void this.activeCdp?.send('Page.screencastFrameAck', { sessionId }).catch(() => undefined);
|
|
687
1014
|
}
|
|
688
1015
|
if (data) {
|
|
689
1016
|
const buffer = Buffer.from(data, 'base64');
|
|
@@ -695,32 +1022,104 @@ export class AgentBrowserService {
|
|
|
695
1022
|
if (message.method === 'Page.frameNavigated') {
|
|
696
1023
|
const frame = (message.params?.frame ?? null);
|
|
697
1024
|
if (frame && !frame.parentId && frame.url) {
|
|
698
|
-
this.
|
|
699
|
-
|
|
1025
|
+
const tab = this.activeTargetId ? this.tabs.get(this.activeTargetId) : null;
|
|
1026
|
+
if (tab) {
|
|
1027
|
+
tab.url = frame.url;
|
|
1028
|
+
}
|
|
1029
|
+
void this.refreshActivePageInfo().finally(() => {
|
|
700
1030
|
void this.broadcastStatus();
|
|
701
1031
|
});
|
|
702
1032
|
}
|
|
703
1033
|
return;
|
|
704
1034
|
}
|
|
705
1035
|
if (message.method === 'Page.loadEventFired') {
|
|
706
|
-
void this.
|
|
1036
|
+
void this.refreshActivePageInfo().finally(() => {
|
|
707
1037
|
void this.broadcastStatus();
|
|
708
1038
|
});
|
|
709
1039
|
}
|
|
710
1040
|
}
|
|
711
|
-
|
|
712
|
-
|
|
1041
|
+
// Events from the browser-level CDP connection: tab lifecycle.
|
|
1042
|
+
handleBrowserEvent(message) {
|
|
1043
|
+
const method = message.method;
|
|
1044
|
+
if (method === 'Target.targetCreated' || method === 'Target.targetInfoChanged') {
|
|
1045
|
+
const info = message.params?.targetInfo;
|
|
1046
|
+
if (info) {
|
|
1047
|
+
this.upsertTab(info);
|
|
1048
|
+
}
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (method === 'Target.targetDestroyed') {
|
|
1052
|
+
const targetId = typeof message.params?.targetId === 'string' ? message.params.targetId : null;
|
|
1053
|
+
if (targetId) {
|
|
1054
|
+
this.removeTab(targetId);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
upsertTab(info) {
|
|
1059
|
+
if (info.type !== 'page') {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
const url = info.url ?? '';
|
|
1063
|
+
if (url.startsWith('devtools://')) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const prev = this.tabs.get(info.targetId);
|
|
1067
|
+
const isNew = !prev;
|
|
1068
|
+
const title = info.title ?? '';
|
|
1069
|
+
const changed = !prev || prev.url !== url || prev.title !== title;
|
|
1070
|
+
this.tabs.set(info.targetId, { id: info.targetId, url, title });
|
|
1071
|
+
if (isNew && this.ready && this.activeTargetId !== info.targetId) {
|
|
1072
|
+
// A page spawned a new tab/popup (target=_blank, window.open) — follow it
|
|
1073
|
+
// the way a normal browser does.
|
|
1074
|
+
void this.setActiveTab(info.targetId);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (changed) {
|
|
1078
|
+
void this.broadcastStatus();
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
removeTab(targetId) {
|
|
1082
|
+
if (!this.tabs.has(targetId)) {
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
const ids = [...this.tabs.keys()];
|
|
1086
|
+
const idx = ids.indexOf(targetId);
|
|
1087
|
+
this.tabs.delete(targetId);
|
|
1088
|
+
if (this.activeTargetId === targetId) {
|
|
1089
|
+
this.activeTargetId = null;
|
|
1090
|
+
this.disconnectActiveCdp();
|
|
1091
|
+
this.clearActiveFrame();
|
|
1092
|
+
this.broadcast({ type: 'clear' });
|
|
1093
|
+
const nextId = ids[idx + 1] ?? ids[idx - 1] ?? null;
|
|
1094
|
+
if (nextId && this.tabs.has(nextId)) {
|
|
1095
|
+
void this.setActiveTab(nextId);
|
|
1096
|
+
return; // setActiveTab broadcasts status
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
void this.broadcastStatus();
|
|
1100
|
+
}
|
|
1101
|
+
clearActiveFrame() {
|
|
1102
|
+
this.lastFrameBuffer = null;
|
|
1103
|
+
this.pendingFrames.clear();
|
|
1104
|
+
}
|
|
1105
|
+
async refreshActivePageInfo() {
|
|
1106
|
+
if (!this.activeCdp || !this.activeTargetId) {
|
|
713
1107
|
return;
|
|
714
1108
|
}
|
|
715
1109
|
try {
|
|
716
|
-
const result = await this.
|
|
1110
|
+
const result = await this.activeCdp.send('Runtime.evaluate', {
|
|
717
1111
|
expression: '({ title: document.title, url: location.href })',
|
|
718
1112
|
returnByValue: true,
|
|
719
1113
|
});
|
|
720
1114
|
const value = result.result?.value;
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1115
|
+
const tab = this.tabs.get(this.activeTargetId);
|
|
1116
|
+
if (value && tab) {
|
|
1117
|
+
if (typeof value.title === 'string') {
|
|
1118
|
+
tab.title = value.title;
|
|
1119
|
+
}
|
|
1120
|
+
if (typeof value.url === 'string') {
|
|
1121
|
+
tab.url = value.url;
|
|
1122
|
+
}
|
|
724
1123
|
}
|
|
725
1124
|
}
|
|
726
1125
|
catch {
|
|
@@ -841,11 +1240,11 @@ export class AgentBrowserService {
|
|
|
841
1240
|
}
|
|
842
1241
|
}
|
|
843
1242
|
}
|
|
844
|
-
|
|
845
|
-
const cdp = this.
|
|
846
|
-
const socket = this.
|
|
847
|
-
this.
|
|
848
|
-
this.
|
|
1243
|
+
disconnectActiveCdp() {
|
|
1244
|
+
const cdp = this.activeCdp;
|
|
1245
|
+
const socket = this.activeCdpSocket;
|
|
1246
|
+
this.activeCdp = null;
|
|
1247
|
+
this.activeCdpSocket = null;
|
|
849
1248
|
this.screencastStarted = false;
|
|
850
1249
|
if (cdp) {
|
|
851
1250
|
cdp.close();
|
|
@@ -859,6 +1258,23 @@ export class AgentBrowserService {
|
|
|
859
1258
|
}
|
|
860
1259
|
}
|
|
861
1260
|
}
|
|
1261
|
+
closeBrowserCdp() {
|
|
1262
|
+
const cdp = this.browserCdp;
|
|
1263
|
+
const socket = this.browserCdpSocket;
|
|
1264
|
+
this.browserCdp = null;
|
|
1265
|
+
this.browserCdpSocket = null;
|
|
1266
|
+
if (cdp) {
|
|
1267
|
+
cdp.close();
|
|
1268
|
+
}
|
|
1269
|
+
else if (socket) {
|
|
1270
|
+
try {
|
|
1271
|
+
socket.close();
|
|
1272
|
+
}
|
|
1273
|
+
catch {
|
|
1274
|
+
// ignore
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
862
1278
|
openLogStream() {
|
|
863
1279
|
this.closeLogStream();
|
|
864
1280
|
const logPath = path.join(this.outputDir, 'deckide-browser.log');
|