lordofc commited on
Commit
1ea5d45
·
verified ·
1 Parent(s): fb01631

Update lib/browser.js

Browse files
Files changed (1) hide show
  1. lib/browser.js +274 -269
lib/browser.js CHANGED
@@ -1,270 +1,275 @@
1
- const { Logger } = require("./logger");
2
- const { connect } = require("puppeteer-real-browser");
3
- const os = require("os");
4
-
5
- class BrowserService {
6
- constructor() {
7
- this.logger = new Logger("Browser");
8
- this.browser = null;
9
- this.browserContexts = new Set();
10
- this.cleanupTimer = null;
11
- this.isShuttingDown = false;
12
- this.reconnectAttempts = 0;
13
- this.maxReconnectAttempts = 5;
14
- this.cleanupInterval = 30000;
15
- this.contextTimeout = 300000;
16
- this.contextCreationTimes = new Map();
17
- this.stats = {
18
- totalContexts: 0,
19
- activeContexts: 0,
20
- memoryUsage: 0,
21
- cpuUsage: 0,
22
- lastCleanup: Date.now(),
23
- };
24
- const cpuCores = os.cpus().length;
25
- this.contextLimit = Math.max(cpuCores * 4, 16);
26
- this.logger.info(`Browser service initialized with context limit: ${this.contextLimit}`);
27
- this.setupGracefulShutdown();
28
- this.startPeriodicCleanup();
29
- }
30
- async initialize(options = {}) {
31
- if (this.isShuttingDown) return;
32
- try {
33
- await this.closeBrowser();
34
- this.logger.info("Launching browser...");
35
- const defaultWidth = 1024;
36
- const defaultHeight = 768;
37
- const width = options.width || defaultWidth;
38
- const height = options.height || defaultHeight;
39
- const { browser } = await connect({
40
- headless: false,
41
- turnstile: true,
42
- connectOption: {
43
- defaultViewport: { width, height },
44
- timeout: 120000,
45
- protocolTimeout: 300000,
46
- args: [
47
- `--window-size=${width},${height}`,
48
- "--no-sandbox",
49
- "--disable-setuid-sandbox",
50
- "--disable-dev-shm-usage",
51
- "--disable-gpu",
52
- "--no-first-run",
53
- "--disable-extensions",
54
- "--disable-sync",
55
- "--disable-translate",
56
- "--disable-web-security",
57
- "--disable-features=VizDisplayCompositor",
58
- ],
59
- },
60
- disableXvfb: false,
61
- });
62
- if (!browser) throw new Error("Failed to connect to browser");
63
- this.browser = browser;
64
- this.reconnectAttempts = 0;
65
- this.setupBrowserEventHandlers();
66
- this.wrapBrowserMethods();
67
- this.logger.success("Browser launched successfully");
68
- } catch (error) {
69
- this.logger.error("Browser initialization failed:", error);
70
- if (this.reconnectAttempts < this.maxReconnectAttempts && !this.isShuttingDown) {
71
- this.reconnectAttempts++;
72
- this.logger.warn(`Retrying browser initialization (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
73
- await new Promise((resolve) => setTimeout(resolve, 5000 * this.reconnectAttempts));
74
- return this.initialize(options);
75
- }
76
- throw error;
77
- }
78
- }
79
-
80
- setupBrowserEventHandlers() {
81
- if (!this.browser) return;
82
- this.browser.on("disconnected", async () => {
83
- if (this.isShuttingDown) return;
84
- this.logger.warn("Browser disconnected, attempting to reconnect...");
85
- await this.handleBrowserDisconnection();
86
- });
87
- this.browser.on("targetcreated", () => this.updateStats());
88
- this.browser.on("targetdestroyed", () => this.updateStats());
89
- }
90
-
91
- wrapBrowserMethods() {
92
- if (!this.browser) return;
93
- const originalCreateContext = this.browser.createBrowserContext.bind(this.browser);
94
- this.browser.createBrowserContext = async (...args) => {
95
- if (this.browserContexts.size >= this.contextLimit) {
96
- await this.forceCleanupOldContexts();
97
- if (this.browserContexts.size >= this.contextLimit) {
98
- throw new Error(`Browser context limit reached (${this.contextLimit})`);
99
- }
100
- }
101
- const context = await originalCreateContext(...args);
102
- if (context) {
103
- this.browserContexts.add(context);
104
- this.contextCreationTimes.set(context, Date.now());
105
- this.stats.totalContexts++;
106
- const originalClose = context.close.bind(context);
107
- context.close = async () => {
108
- try {
109
- await originalClose();
110
- } catch (error) {
111
- this.logger.warn("Error closing context:", error.message);
112
- } finally {
113
- this.browserContexts.delete(context);
114
- this.contextCreationTimes.delete(context);
115
- this.updateStats();
116
- }
117
- };
118
-
119
- setTimeout(async () => {
120
- if (this.browserContexts.has(context)) {
121
- this.logger.debug("Force closing expired context");
122
- try {
123
- await context.close();
124
- } catch { }
125
- }
126
- }, this.contextTimeout);
127
- }
128
- this.updateStats();
129
- return context;
130
- };
131
- }
132
-
133
- async handleBrowserDisconnection() {
134
- try {
135
- const cleanupPromises = Array.from(this.browserContexts).map((context) => context.close().catch(() => { }));
136
- await Promise.allSettled(cleanupPromises);
137
- this.browserContexts.clear();
138
- this.contextCreationTimes.clear();
139
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
140
- await new Promise((resolve) => setTimeout(resolve, 5000));
141
- await this.initialize();
142
- } else {
143
- this.logger.error("Max reconnection attempts reached");
144
- }
145
- } catch (error) {
146
- this.logger.error("Error handling browser disconnection:", error);
147
- }
148
- }
149
-
150
- startPeriodicCleanup() {
151
- this.cleanupTimer = setInterval(async () => {
152
- if (this.isShuttingDown) return;
153
- try {
154
- await this.performCleanup();
155
- this.updateStats();
156
- } catch (error) {
157
- this.logger.error("Periodic cleanup error:", error);
158
- }
159
- }, this.cleanupInterval);
160
- }
161
-
162
- async performCleanup() {
163
- const now = Date.now();
164
- const contextsToCleanup = [];
165
- for (const [context, creationTime] of this.contextCreationTimes.entries()) {
166
- if (now - creationTime > this.contextTimeout) {
167
- contextsToCleanup.push(context);
168
- }
169
- }
170
- if (contextsToCleanup.length > 0) {
171
- this.logger.debug(`Cleaning up ${contextsToCleanup.length} expired contexts`);
172
- const cleanupPromises = contextsToCleanup.map((context) => context.close().catch(() => { }));
173
- await Promise.allSettled(cleanupPromises);
174
- }
175
- if (this.browserContexts.size > this.contextLimit * 0.8) await this.forceCleanupOldContexts();
176
- this.stats.lastCleanup = now;
177
- }
178
-
179
- async forceCleanupOldContexts() {
180
- const contextsArray = Array.from(this.browserContexts);
181
- const sortedContexts = contextsArray.sort((a, b) => {
182
- const timeA = this.contextCreationTimes.get(a) || 0;
183
- const timeB = this.contextCreationTimes.get(b) || 0;
184
- return timeA - timeB;
185
- });
186
- const toCleanup = sortedContexts.slice(0, Math.floor(sortedContexts.length * 0.3));
187
- if (toCleanup.length > 0) {
188
- this.logger.warn(`Force cleaning up ${toCleanup.length} contexts due to limit`);
189
- const cleanupPromises = toCleanup.map((context) => context.close().catch(() => { }));
190
- await Promise.allSettled(cleanupPromises);
191
- }
192
- }
193
-
194
- updateStats() {
195
- this.stats.activeContexts = this.browserContexts.size;
196
- this.stats.memoryUsage = process.memoryUsage().heapUsed;
197
- const usage = process.cpuUsage();
198
- this.stats.cpuUsage = (usage.user + usage.system) / 1000000;
199
- }
200
-
201
- async createContext(options = {}) {
202
- if (!this.browser) await this.initialize();
203
- if (!this.browser) throw new Error("Browser not available");
204
- return await this.browser.createBrowserContext({
205
- ...options,
206
- ignoreHTTPSErrors: true,
207
- });
208
- }
209
-
210
- async withBrowserContext(callback) {
211
- let context = null;
212
- try {
213
- context = await this.createContext();
214
- return await callback(context);
215
- } finally {
216
- if (context) {
217
- try {
218
- await context.close();
219
- } catch (error) {
220
- this.logger.warn(`Failed to close context: ${error.message}`);
221
- }
222
- }
223
- }
224
- }
225
-
226
- getBrowserStats() {
227
- return { ...this.stats };
228
- }
229
-
230
- isReady() {
231
- return this.browser !== null && !this.isShuttingDown;
232
- }
233
-
234
- async closeBrowser() {
235
- if (this.browser) {
236
- try {
237
- const cleanupPromises = Array.from(this.browserContexts).map((context) => context.close().catch(() => { }));
238
- await Promise.allSettled(cleanupPromises);
239
- this.browserContexts.clear();
240
- this.contextCreationTimes.clear();
241
- await this.browser.close();
242
- this.logger.info("Browser closed successfully");
243
- } catch (error) {
244
- this.logger.error("Error closing browser:", error);
245
- } finally {
246
- this.browser = null;
247
- }
248
- }
249
- }
250
-
251
- setupGracefulShutdown() {
252
- const gracefulShutdown = async (signal) => {
253
- this.logger.warn(`Received ${signal}, shutting down browser service...`);
254
- this.isShuttingDown = true;
255
- if (this.cleanupTimer) clearInterval(this.cleanupTimer);
256
- await this.closeBrowser();
257
- this.logger.success("Browser service shutdown complete");
258
- };
259
- process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
260
- process.on("SIGINT", () => gracefulShutdown("SIGINT"));
261
- }
262
-
263
- async shutdown() {
264
- this.isShuttingDown = true;
265
- if (this.cleanupTimer) clearInterval(this.cleanupTimer);
266
- await this.closeBrowser();
267
- }
268
- }
269
-
 
 
 
 
 
270
  module.exports = { BrowserService };
 
1
+ const { Logger } = require("./logger");
2
+ const { connect } = require("puppeteer-real-browser");
3
+ const os = require("os");
4
+
5
+ class BrowserService {
6
+ constructor() {
7
+ this.logger = new Logger("Browser");
8
+ this.browser = null;
9
+ this.browserContexts = new Set();
10
+ this.cleanupTimer = null;
11
+ this.isShuttingDown = false;
12
+ this.reconnectAttempts = 0;
13
+ this.maxReconnectAttempts = 5;
14
+ this.cleanupInterval = 30000;
15
+ this.contextTimeout = 300000;
16
+ this.contextCreationTimes = new Map();
17
+ this.stats = {
18
+ totalContexts: 0,
19
+ activeContexts: 0,
20
+ memoryUsage: 0,
21
+ cpuUsage: 0,
22
+ lastCleanup: Date.now(),
23
+ };
24
+ const cpuCores = os.cpus().length;
25
+ this.contextLimit = Math.max(cpuCores * 4, 16);
26
+ this.logger.info(`Browser service initialized with context limit: ${this.contextLimit}`);
27
+ this.setupGracefulShutdown();
28
+ this.startPeriodicCleanup();
29
+ }
30
+ async initialize(options = {}) {
31
+ if (this.isShuttingDown) return;
32
+ try {
33
+ await this.closeBrowser();
34
+ this.logger.info("Launching browser...");
35
+ const defaultWidth = 1024;
36
+ const defaultHeight = 768;
37
+ const width = options.width || defaultWidth;
38
+ const height = options.height || defaultHeight;
39
+ const { browser } = await connect({
40
+ headless: false,
41
+ turnstile: true,
42
+ connectOption: {
43
+ defaultViewport: { width, height },
44
+ timeout: 120000,
45
+ protocolTimeout: 300000,
46
+ args: [
47
+ `--window-size=${width},${height}`,
48
+ '--no-sandbox',
49
+ '--disable-setuid-sandbox',
50
+ '--disable-dev-shm-usage',
51
+ '--disable-gpu',
52
+ '--disable-software-rasterizer',
53
+ '--disable-background-networking',
54
+ '--disable-default-apps',
55
+ '--disable-extensions',
56
+ '--disable-sync',
57
+ '--disable-translate',
58
+ '--disable-web-security',
59
+ '--disable-features=VizDisplayCompositor',
60
+ '--single-process',
61
+ '--no-zygote',
62
+ '--no-first-run'
63
+ ],
64
+ },
65
+ disableXvfb: false,
66
+ });
67
+ if (!browser) throw new Error("Failed to connect to browser");
68
+ this.browser = browser;
69
+ this.reconnectAttempts = 0;
70
+ this.setupBrowserEventHandlers();
71
+ this.wrapBrowserMethods();
72
+ this.logger.success("Browser launched successfully");
73
+ } catch (error) {
74
+ this.logger.error("Browser initialization failed:", error);
75
+ if (this.reconnectAttempts < this.maxReconnectAttempts && !this.isShuttingDown) {
76
+ this.reconnectAttempts++;
77
+ this.logger.warn(`Retrying browser initialization (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
78
+ await new Promise((resolve) => setTimeout(resolve, 5000 * this.reconnectAttempts));
79
+ return this.initialize(options);
80
+ }
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ setupBrowserEventHandlers() {
86
+ if (!this.browser) return;
87
+ this.browser.on("disconnected", async () => {
88
+ if (this.isShuttingDown) return;
89
+ this.logger.warn("Browser disconnected, attempting to reconnect...");
90
+ await this.handleBrowserDisconnection();
91
+ });
92
+ this.browser.on("targetcreated", () => this.updateStats());
93
+ this.browser.on("targetdestroyed", () => this.updateStats());
94
+ }
95
+
96
+ wrapBrowserMethods() {
97
+ if (!this.browser) return;
98
+ const originalCreateContext = this.browser.createBrowserContext.bind(this.browser);
99
+ this.browser.createBrowserContext = async (...args) => {
100
+ if (this.browserContexts.size >= this.contextLimit) {
101
+ await this.forceCleanupOldContexts();
102
+ if (this.browserContexts.size >= this.contextLimit) {
103
+ throw new Error(`Browser context limit reached (${this.contextLimit})`);
104
+ }
105
+ }
106
+ const context = await originalCreateContext(...args);
107
+ if (context) {
108
+ this.browserContexts.add(context);
109
+ this.contextCreationTimes.set(context, Date.now());
110
+ this.stats.totalContexts++;
111
+ const originalClose = context.close.bind(context);
112
+ context.close = async () => {
113
+ try {
114
+ await originalClose();
115
+ } catch (error) {
116
+ this.logger.warn("Error closing context:", error.message);
117
+ } finally {
118
+ this.browserContexts.delete(context);
119
+ this.contextCreationTimes.delete(context);
120
+ this.updateStats();
121
+ }
122
+ };
123
+
124
+ setTimeout(async () => {
125
+ if (this.browserContexts.has(context)) {
126
+ this.logger.debug("Force closing expired context");
127
+ try {
128
+ await context.close();
129
+ } catch { }
130
+ }
131
+ }, this.contextTimeout);
132
+ }
133
+ this.updateStats();
134
+ return context;
135
+ };
136
+ }
137
+
138
+ async handleBrowserDisconnection() {
139
+ try {
140
+ const cleanupPromises = Array.from(this.browserContexts).map((context) => context.close().catch(() => { }));
141
+ await Promise.allSettled(cleanupPromises);
142
+ this.browserContexts.clear();
143
+ this.contextCreationTimes.clear();
144
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
145
+ await new Promise((resolve) => setTimeout(resolve, 5000));
146
+ await this.initialize();
147
+ } else {
148
+ this.logger.error("Max reconnection attempts reached");
149
+ }
150
+ } catch (error) {
151
+ this.logger.error("Error handling browser disconnection:", error);
152
+ }
153
+ }
154
+
155
+ startPeriodicCleanup() {
156
+ this.cleanupTimer = setInterval(async () => {
157
+ if (this.isShuttingDown) return;
158
+ try {
159
+ await this.performCleanup();
160
+ this.updateStats();
161
+ } catch (error) {
162
+ this.logger.error("Periodic cleanup error:", error);
163
+ }
164
+ }, this.cleanupInterval);
165
+ }
166
+
167
+ async performCleanup() {
168
+ const now = Date.now();
169
+ const contextsToCleanup = [];
170
+ for (const [context, creationTime] of this.contextCreationTimes.entries()) {
171
+ if (now - creationTime > this.contextTimeout) {
172
+ contextsToCleanup.push(context);
173
+ }
174
+ }
175
+ if (contextsToCleanup.length > 0) {
176
+ this.logger.debug(`Cleaning up ${contextsToCleanup.length} expired contexts`);
177
+ const cleanupPromises = contextsToCleanup.map((context) => context.close().catch(() => { }));
178
+ await Promise.allSettled(cleanupPromises);
179
+ }
180
+ if (this.browserContexts.size > this.contextLimit * 0.8) await this.forceCleanupOldContexts();
181
+ this.stats.lastCleanup = now;
182
+ }
183
+
184
+ async forceCleanupOldContexts() {
185
+ const contextsArray = Array.from(this.browserContexts);
186
+ const sortedContexts = contextsArray.sort((a, b) => {
187
+ const timeA = this.contextCreationTimes.get(a) || 0;
188
+ const timeB = this.contextCreationTimes.get(b) || 0;
189
+ return timeA - timeB;
190
+ });
191
+ const toCleanup = sortedContexts.slice(0, Math.floor(sortedContexts.length * 0.3));
192
+ if (toCleanup.length > 0) {
193
+ this.logger.warn(`Force cleaning up ${toCleanup.length} contexts due to limit`);
194
+ const cleanupPromises = toCleanup.map((context) => context.close().catch(() => { }));
195
+ await Promise.allSettled(cleanupPromises);
196
+ }
197
+ }
198
+
199
+ updateStats() {
200
+ this.stats.activeContexts = this.browserContexts.size;
201
+ this.stats.memoryUsage = process.memoryUsage().heapUsed;
202
+ const usage = process.cpuUsage();
203
+ this.stats.cpuUsage = (usage.user + usage.system) / 1000000;
204
+ }
205
+
206
+ async createContext(options = {}) {
207
+ if (!this.browser) await this.initialize();
208
+ if (!this.browser) throw new Error("Browser not available");
209
+ return await this.browser.createBrowserContext({
210
+ ...options,
211
+ ignoreHTTPSErrors: true,
212
+ });
213
+ }
214
+
215
+ async withBrowserContext(callback) {
216
+ let context = null;
217
+ try {
218
+ context = await this.createContext();
219
+ return await callback(context);
220
+ } finally {
221
+ if (context) {
222
+ try {
223
+ await context.close();
224
+ } catch (error) {
225
+ this.logger.warn(`Failed to close context: ${error.message}`);
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ getBrowserStats() {
232
+ return { ...this.stats };
233
+ }
234
+
235
+ isReady() {
236
+ return this.browser !== null && !this.isShuttingDown;
237
+ }
238
+
239
+ async closeBrowser() {
240
+ if (this.browser) {
241
+ try {
242
+ const cleanupPromises = Array.from(this.browserContexts).map((context) => context.close().catch(() => { }));
243
+ await Promise.allSettled(cleanupPromises);
244
+ this.browserContexts.clear();
245
+ this.contextCreationTimes.clear();
246
+ await this.browser.close();
247
+ this.logger.info("Browser closed successfully");
248
+ } catch (error) {
249
+ this.logger.error("Error closing browser:", error);
250
+ } finally {
251
+ this.browser = null;
252
+ }
253
+ }
254
+ }
255
+
256
+ setupGracefulShutdown() {
257
+ const gracefulShutdown = async (signal) => {
258
+ this.logger.warn(`Received ${signal}, shutting down browser service...`);
259
+ this.isShuttingDown = true;
260
+ if (this.cleanupTimer) clearInterval(this.cleanupTimer);
261
+ await this.closeBrowser();
262
+ this.logger.success("Browser service shutdown complete");
263
+ };
264
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
265
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
266
+ }
267
+
268
+ async shutdown() {
269
+ this.isShuttingDown = true;
270
+ if (this.cleanupTimer) clearInterval(this.cleanupTimer);
271
+ await this.closeBrowser();
272
+ }
273
+ }
274
+
275
  module.exports = { BrowserService };