|
@@ -5,33 +5,43 @@ export interface BrowserOptions {
|
|
|
headless?: boolean;
|
|
headless?: boolean;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** 无头浏览器空闲超时(毫秒),超时后自动关闭,节省资源 */
|
|
|
|
|
+const HEADLESS_IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 分钟空闲后关闭
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 浏览器管理器
|
|
* 浏览器管理器
|
|
|
* 支持有头和无头两种模式的浏览器实例
|
|
* 支持有头和无头两种模式的浏览器实例
|
|
|
|
|
+ * 无头浏览器在空闲一定时间后自动关闭,节省资源
|
|
|
*/
|
|
*/
|
|
|
export class BrowserManager {
|
|
export class BrowserManager {
|
|
|
// 有头浏览器(用于用户交互,如登录)
|
|
// 有头浏览器(用于用户交互,如登录)
|
|
|
private static headfulBrowser: Browser | null = null;
|
|
private static headfulBrowser: Browser | null = null;
|
|
|
private static isHeadfulInitializing = false;
|
|
private static isHeadfulInitializing = false;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 无头浏览器(用于后台任务,如发布视频)
|
|
// 无头浏览器(用于后台任务,如发布视频)
|
|
|
private static headlessBrowser: Browser | null = null;
|
|
private static headlessBrowser: Browser | null = null;
|
|
|
private static isHeadlessInitializing = false;
|
|
private static isHeadlessInitializing = false;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /** 无头浏览器最后使用时间戳 */
|
|
|
|
|
+ private static headlessLastUsed = 0;
|
|
|
|
|
+
|
|
|
|
|
+ /** 无头浏览器空闲关闭定时器 */
|
|
|
|
|
+ private static headlessIdleTimer: NodeJS.Timeout | null = null;
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 获取浏览器实例
|
|
* 获取浏览器实例
|
|
|
* @param options.headless 是否使用无头模式,默认 false
|
|
* @param options.headless 是否使用无头模式,默认 false
|
|
|
*/
|
|
*/
|
|
|
static async getBrowser(options?: BrowserOptions): Promise<Browser> {
|
|
static async getBrowser(options?: BrowserOptions): Promise<Browser> {
|
|
|
const headless = options?.headless ?? false;
|
|
const headless = options?.headless ?? false;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if (headless) {
|
|
if (headless) {
|
|
|
return this.getHeadlessBrowser();
|
|
return this.getHeadlessBrowser();
|
|
|
} else {
|
|
} else {
|
|
|
return this.getHeadfulBrowser();
|
|
return this.getHeadfulBrowser();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 获取有头浏览器实例(用于用户交互)
|
|
* 获取有头浏览器实例(用于用户交互)
|
|
|
*/
|
|
*/
|
|
@@ -39,18 +49,18 @@ export class BrowserManager {
|
|
|
if (this.headfulBrowser && this.headfulBrowser.isConnected()) {
|
|
if (this.headfulBrowser && this.headfulBrowser.isConnected()) {
|
|
|
return this.headfulBrowser;
|
|
return this.headfulBrowser;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 防止并发初始化
|
|
// 防止并发初始化
|
|
|
if (this.isHeadfulInitializing) {
|
|
if (this.isHeadfulInitializing) {
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
return this.getHeadfulBrowser();
|
|
return this.getHeadfulBrowser();
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
this.isHeadfulInitializing = true;
|
|
this.isHeadfulInitializing = true;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
logger.info('Launching headful browser...');
|
|
logger.info('Launching headful browser...');
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
this.headfulBrowser = await chromium.launch({
|
|
this.headfulBrowser = await chromium.launch({
|
|
|
headless: false,
|
|
headless: false,
|
|
|
args: [
|
|
args: [
|
|
@@ -62,12 +72,12 @@ export class BrowserManager {
|
|
|
'--window-size=1920,1080',
|
|
'--window-size=1920,1080',
|
|
|
],
|
|
],
|
|
|
});
|
|
});
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
this.headfulBrowser.on('disconnected', () => {
|
|
this.headfulBrowser.on('disconnected', () => {
|
|
|
logger.warn('Headful browser disconnected');
|
|
logger.warn('Headful browser disconnected');
|
|
|
this.headfulBrowser = null;
|
|
this.headfulBrowser = null;
|
|
|
});
|
|
});
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
logger.info('Headful browser launched successfully');
|
|
logger.info('Headful browser launched successfully');
|
|
|
return this.headfulBrowser;
|
|
return this.headfulBrowser;
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
@@ -77,26 +87,34 @@ export class BrowserManager {
|
|
|
this.isHeadfulInitializing = false;
|
|
this.isHeadfulInitializing = false;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 获取无头浏览器实例(用于后台任务)
|
|
* 获取无头浏览器实例(用于后台任务)
|
|
|
|
|
+ * 有空时自动关闭机制,减少资源占用
|
|
|
*/
|
|
*/
|
|
|
private static async getHeadlessBrowser(): Promise<Browser> {
|
|
private static async getHeadlessBrowser(): Promise<Browser> {
|
|
|
|
|
+ // 清除之前的空闲关闭定时器
|
|
|
|
|
+ if (this.headlessIdleTimer) {
|
|
|
|
|
+ clearTimeout(this.headlessIdleTimer);
|
|
|
|
|
+ this.headlessIdleTimer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (this.headlessBrowser && this.headlessBrowser.isConnected()) {
|
|
if (this.headlessBrowser && this.headlessBrowser.isConnected()) {
|
|
|
|
|
+ this.headlessLastUsed = Date.now();
|
|
|
return this.headlessBrowser;
|
|
return this.headlessBrowser;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 防止并发初始化
|
|
// 防止并发初始化
|
|
|
if (this.isHeadlessInitializing) {
|
|
if (this.isHeadlessInitializing) {
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
return this.getHeadlessBrowser();
|
|
return this.getHeadlessBrowser();
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
this.isHeadlessInitializing = true;
|
|
this.isHeadlessInitializing = true;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
logger.info('Launching headless browser...');
|
|
logger.info('Launching headless browser...');
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
this.headlessBrowser = await chromium.launch({
|
|
this.headlessBrowser = await chromium.launch({
|
|
|
headless: true,
|
|
headless: true,
|
|
|
args: [
|
|
args: [
|
|
@@ -106,14 +124,22 @@ export class BrowserManager {
|
|
|
'--disable-accelerated-2d-canvas',
|
|
'--disable-accelerated-2d-canvas',
|
|
|
'--disable-gpu',
|
|
'--disable-gpu',
|
|
|
'--window-size=1920,1080',
|
|
'--window-size=1920,1080',
|
|
|
|
|
+ // 限制 Chromium 子进程数,降低资源占用
|
|
|
|
|
+ '--max-process-count=3',
|
|
|
],
|
|
],
|
|
|
});
|
|
});
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ this.headlessLastUsed = Date.now();
|
|
|
|
|
+
|
|
|
this.headlessBrowser.on('disconnected', () => {
|
|
this.headlessBrowser.on('disconnected', () => {
|
|
|
logger.warn('Headless browser disconnected');
|
|
logger.warn('Headless browser disconnected');
|
|
|
this.headlessBrowser = null;
|
|
this.headlessBrowser = null;
|
|
|
|
|
+ this.headlessLastUsed = 0;
|
|
|
});
|
|
});
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 启动空闲关闭定时器
|
|
|
|
|
+ this.scheduleIdleClose();
|
|
|
|
|
+
|
|
|
logger.info('Headless browser launched successfully');
|
|
logger.info('Headless browser launched successfully');
|
|
|
return this.headlessBrowser;
|
|
return this.headlessBrowser;
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
@@ -123,7 +149,30 @@ export class BrowserManager {
|
|
|
this.isHeadlessInitializing = false;
|
|
this.isHeadlessInitializing = false;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 安排空闲关闭定时器(无头浏览器在空闲超时后自动关闭)
|
|
|
|
|
+ */
|
|
|
|
|
+ private static scheduleIdleClose(): void {
|
|
|
|
|
+ if (this.headlessIdleTimer) {
|
|
|
|
|
+ clearTimeout(this.headlessIdleTimer);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.headlessIdleTimer = setTimeout(async () => {
|
|
|
|
|
+ if (this.headlessBrowser && this.headlessBrowser.isConnected()) {
|
|
|
|
|
+ const idle = Date.now() - this.headlessLastUsed;
|
|
|
|
|
+ if (idle >= HEADLESS_IDLE_TIMEOUT_MS) {
|
|
|
|
|
+ logger.info(`[BrowserManager] Headless browser idle for ${Math.round(idle / 1000)}s, closing to save resources...`);
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.headlessBrowser.close();
|
|
|
|
|
+ } catch {}
|
|
|
|
|
+ this.headlessBrowser = null;
|
|
|
|
|
+ this.headlessLastUsed = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ this.headlessIdleTimer = null;
|
|
|
|
|
+ }, HEADLESS_IDLE_TIMEOUT_MS);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 关闭浏览器
|
|
* 关闭浏览器
|
|
|
* @param options.headless 指定关闭哪种浏览器,不指定则关闭所有
|
|
* @param options.headless 指定关闭哪种浏览器,不指定则关闭所有
|
|
@@ -131,9 +180,14 @@ export class BrowserManager {
|
|
|
static async closeBrowser(options?: BrowserOptions): Promise<void> {
|
|
static async closeBrowser(options?: BrowserOptions): Promise<void> {
|
|
|
if (options?.headless === true) {
|
|
if (options?.headless === true) {
|
|
|
// 只关闭无头浏览器
|
|
// 只关闭无头浏览器
|
|
|
|
|
+ if (this.headlessIdleTimer) {
|
|
|
|
|
+ clearTimeout(this.headlessIdleTimer);
|
|
|
|
|
+ this.headlessIdleTimer = null;
|
|
|
|
|
+ }
|
|
|
if (this.headlessBrowser) {
|
|
if (this.headlessBrowser) {
|
|
|
await this.headlessBrowser.close();
|
|
await this.headlessBrowser.close();
|
|
|
this.headlessBrowser = null;
|
|
this.headlessBrowser = null;
|
|
|
|
|
+ this.headlessLastUsed = 0;
|
|
|
logger.info('Headless browser closed');
|
|
logger.info('Headless browser closed');
|
|
|
}
|
|
}
|
|
|
} else if (options?.headless === false) {
|
|
} else if (options?.headless === false) {
|
|
@@ -151,13 +205,18 @@ export class BrowserManager {
|
|
|
logger.info('Headful browser closed');
|
|
logger.info('Headful browser closed');
|
|
|
}
|
|
}
|
|
|
if (this.headlessBrowser) {
|
|
if (this.headlessBrowser) {
|
|
|
|
|
+ if (this.headlessIdleTimer) {
|
|
|
|
|
+ clearTimeout(this.headlessIdleTimer);
|
|
|
|
|
+ this.headlessIdleTimer = null;
|
|
|
|
|
+ }
|
|
|
await this.headlessBrowser.close();
|
|
await this.headlessBrowser.close();
|
|
|
this.headlessBrowser = null;
|
|
this.headlessBrowser = null;
|
|
|
|
|
+ this.headlessLastUsed = 0;
|
|
|
logger.info('Headless browser closed');
|
|
logger.info('Headless browser closed');
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 检查浏览器状态
|
|
* 检查浏览器状态
|
|
|
*/
|
|
*/
|
|
@@ -168,7 +227,7 @@ export class BrowserManager {
|
|
|
return this.headfulBrowser?.isConnected() ?? false;
|
|
return this.headfulBrowser?.isConnected() ?? false;
|
|
|
}
|
|
}
|
|
|
// 任一浏览器连接即返回 true
|
|
// 任一浏览器连接即返回 true
|
|
|
- return (this.headfulBrowser?.isConnected() ?? false) ||
|
|
|
|
|
|
|
+ return (this.headfulBrowser?.isConnected() ?? false) ||
|
|
|
(this.headlessBrowser?.isConnected() ?? false);
|
|
(this.headlessBrowser?.isConnected() ?? false);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|