|
24 | 24 | * (Kelvin Yuli Andrian's own implementation, which proves this works) |
25 | 25 | */ |
26 | 26 |
|
| 27 | +import { existsSync, unlinkSync } from 'fs'; |
| 28 | +import { join } from 'path'; |
| 29 | +import { execSync } from 'child_process'; |
| 30 | + |
27 | 31 | import { Logger } from '@config/logger.config'; |
28 | 32 | import { BadRequestException } from '@exceptions'; |
29 | 33 | import puppeteer, { Browser, Page } from 'puppeteer-core'; |
@@ -520,17 +524,36 @@ export class BrowserCatalogService { |
520 | 524 | const userDataDir = this.sessionStore.userDataDir(instanceName); |
521 | 525 | this.logger.log(`[browser] Launching Chromium for instance=${instanceName} jid=${jid}`); |
522 | 526 |
|
| 527 | + // Clean up stale SingletonLock file from previous crashed launches. |
| 528 | + // Chromium creates this lock file to prevent concurrent profile access, |
| 529 | + // but if a previous process crashed, the lock stays and blocks new launches. |
| 530 | + this.cleanStaleLocks(userDataDir); |
| 531 | + |
523 | 532 | const browser = await puppeteer.launch({ |
524 | 533 | executablePath: this.config.executablePath, |
525 | 534 | headless: this.config.headless, |
526 | 535 | userDataDir, |
527 | 536 | args: this.config.extraArgs, |
528 | 537 | defaultViewport: { width: 1280, height: 800 }, |
529 | 538 | ignoreDefaultArgs: ['--enable-automation'], |
| 539 | + // Wait for initial page to be ready before returning |
| 540 | + protocolTimeout: 60000, |
530 | 541 | }); |
531 | 542 |
|
532 | 543 | this.browsers.set(jid, browser); |
533 | 544 |
|
| 545 | + // Handle unexpected browser disconnect — clean up so next call can re-launch |
| 546 | + browser.on('disconnected', () => { |
| 547 | + this.logger.warn(`[browser] Browser disconnected for jid=${jid}, cleaning up`); |
| 548 | + this.browsers.delete(jid); |
| 549 | + const timer = this.idleTimers.get(jid); |
| 550 | + if (timer) { |
| 551 | + clearTimeout(timer); |
| 552 | + this.idleTimers.delete(jid); |
| 553 | + } |
| 554 | + this.pendingQr.delete(jid); |
| 555 | + }); |
| 556 | + |
534 | 557 | // Navigate to WA Web on the first page |
535 | 558 | const page = await browser.newPage(); |
536 | 559 | await page.goto('https://web.whatsapp.com/', { |
@@ -602,6 +625,40 @@ export class BrowserCatalogService { |
602 | 625 | this.idleTimers.set(jid, timer); |
603 | 626 | } |
604 | 627 |
|
| 628 | + /** |
| 629 | + * Remove stale Chromium lock files and kill orphan Chromium processes |
| 630 | + * left over from previous crashed launches. |
| 631 | + * |
| 632 | + * Chromium creates SingletonLock, SingletonCookie, and SingletonSocket |
| 633 | + * symlinks in the userDataDir. If a previous process crashed, these |
| 634 | + * locks persist and block new launches with "profile appears to be in |
| 635 | + * use by another Chromium process" error. |
| 636 | + */ |
| 637 | + private cleanStaleLocks(userDataDir: string): void { |
| 638 | + // 1. Remove lock files/symlinks |
| 639 | + const lockFiles = ['SingletonLock', 'SingletonCookie', 'SingletonSocket']; |
| 640 | + for (const lockFile of lockFiles) { |
| 641 | + const lockPath = join(userDataDir, lockFile); |
| 642 | + if (existsSync(lockPath)) { |
| 643 | + try { |
| 644 | + unlinkSync(lockPath); |
| 645 | + this.logger.log(`[browser] Removed stale lock: ${lockFile}`); |
| 646 | + } catch (err) { |
| 647 | + this.logger.warn(`[browser] Failed to remove ${lockFile}: ${(err as Error).message}`); |
| 648 | + } |
| 649 | + } |
| 650 | + } |
| 651 | + |
| 652 | + // 2. Kill orphan chromium processes (best-effort, ignore errors) |
| 653 | + // This handles the case where a previous Puppeteer crash left |
| 654 | + // chromium processes running and holding the profile. |
| 655 | + try { |
| 656 | + execSync('pkill -f chromium 2>/dev/null || true', { timeout: 5000 }); |
| 657 | + } catch { |
| 658 | + // pkill exit code 1 = no process matched, ignore |
| 659 | + } |
| 660 | + } |
| 661 | + |
605 | 662 | /** |
606 | 663 | * Kill the browser for a JID and clean up timers. |
607 | 664 | */ |
|
0 commit comments