Skip to content

Commit f99a126

Browse files
committed
Refactor server and session management
- Remove metrics and system info modules - Simplify browser session handling - Update Supabase authentication to return user ID - Modify Chrome arguments and environment configuration - Update package scripts to use environment file
1 parent a0d44f6 commit f99a126

18 files changed

+329
-217
lines changed

.env.example

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
CHROME_PATH=/usr/bin/google-chrome
33

44
# Defines Chrome arguments for security and privacy
5-
CHROME_ARGS="--no-sandbox --incognito --disable-sync --disable-logging --disable-ftp --disable-password-generation --disable-save-password-bubble --disable-autofill --disable-translate"
5+
CHROME_ARGS="--no-sandbox --enable-zero-copy"
66

77
# Defines the database access credentials
88
SUPABASE_SERVICE_ROLE_KEY=xxx
99
SUPABASE_URL=xxx
1010

11-
# Defines the GCP project ID
11+
# Defines the GCP project ID (optional)
1212
GCP_PROJECT_ID=xxx

capture-gpu.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import puppeteer from 'puppeteer-core';
33
(async () => {
44
// Connect to the existing Chrome instance via WebSocket
55
const browser = await puppeteer.connect({
6-
browserWSEndpoint: 'ws://localhost:3000?token=run-npm-run-keygen-to-generate-api-key',
6+
browserWSEndpoint: `ws://localhost:3000?token=${process.env.API_KEY}`,
77
})
88

99
// Create a new page

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
"dev": "tsx --watch --env-file=.env src/app.ts",
99
"serve": "tsx src/app.ts",
1010
"keygen": "tsx keygen.ts",
11-
"gpuinfo": "tsx capture-gpu.ts"
11+
"gpuinfo": "tsx --env-file=.env capture-gpu.ts"
1212
},
1313
"devDependencies": {
1414
"@types/express": "^5.0.0",
1515
"@types/node": "^22.2.0",
16-
"@types/ws": "^8.5.13",
16+
"@types/ws": "^8.5.14",
1717
"esbuild": "^0.24.2",
1818
"tsx": "^4.19.2",
1919
"typescript": "^5.5.3"

src/app.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import wsserver from "./server";
55
import * as sessions from "./routes/sessions";
66
import * as health from "./routes/health";
77
import * as env from "./environment";
8+
import './utilization';
89

910
export const app = express();
1011
export const server = http.createServer(app);
@@ -21,5 +22,3 @@ server.on('upgrade', wsserver);
2122
server.listen(env.PORT, async () => {
2223
console.log(`Server running on port ${env.PORT}`);
2324
});
24-
25-
import('./metrics').then(module => module.default());

src/browser.ts

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import puppeteer, { Browser as PuppeteerBrowser, Target } from 'puppeteer-core';
2+
import { MAX_BROWSER_INSTANCES, CHROME_PATH, CHROME_ARGS } from './environment';
3+
import { CHROME_SECURITY_FLAGS } from './constants';
4+
import { report } from './monitoring';
5+
import { v4 as uuid } from 'uuid';
6+
import { supabase } from './lib/supabase';
7+
8+
export const browsers = new Map<string, Browser>();
9+
10+
export class Browser {
11+
public readonly user?: string | null;
12+
public readonly id: string = uuid();
13+
public instance?: PuppeteerBrowser;
14+
public pages: string[] = [];
15+
16+
public constructor(user?: string | null) {
17+
this.user = user;
18+
}
19+
20+
public async init() {
21+
try {
22+
const { error } = await supabase
23+
.from('browsers')
24+
.insert([{ id: this.id, user: this.user }])
25+
.single();
26+
27+
if (error) {
28+
console.error('Error creating browser database entry:', error);
29+
return;
30+
}
31+
32+
this.instance = await puppeteer.launch({
33+
executablePath: CHROME_PATH,
34+
headless: true,
35+
args: [...CHROME_SECURITY_FLAGS, ...CHROME_ARGS],
36+
});
37+
38+
this.instance.on('targetcreated', this.handlePageCreated.bind(this));
39+
console.log('Initialized browser:', this.id);
40+
} catch (e) {
41+
console.error('Error initializing browser:', e);
42+
} finally {
43+
browsers.set(this.id, this);
44+
this.handleUtilizationReport();
45+
}
46+
}
47+
48+
public async close() {
49+
if (!this.instance) return;
50+
51+
try {
52+
await this.instance.close();
53+
console.log('Closed browser:', this.id);
54+
} catch (e) {
55+
console.error('Error closing browser:', e);
56+
} finally {
57+
let error = await supabase
58+
.from('browsers')
59+
.update({ closed_at: new Date().toISOString() })
60+
.is('closed_at', null)
61+
.eq('id', this.id)
62+
.single()
63+
.then(res => res.error);
64+
65+
if (error) {
66+
console.error('Error updating browser closed_at:', error);
67+
}
68+
69+
error = await supabase
70+
.from('browser_tabs')
71+
.update([{ browser: this.id, closed_at: new Date().toISOString() }])
72+
.eq('browser', this.id)
73+
.then(res => res.error);
74+
75+
if (error) {
76+
console.error('Error updating browser tabs:', error);
77+
}
78+
79+
browsers.delete(this.id);
80+
this.instance = undefined;
81+
this.handleUtilizationReport();
82+
}
83+
}
84+
85+
private async handlePageCreated(target: Target) {
86+
if (target.type() !== 'page') return;
87+
88+
const page = await target.page();
89+
90+
const { data, error } = await supabase
91+
.from('browser_tabs')
92+
.insert([{ browser: this.id }])
93+
.select()
94+
.single();
95+
96+
if (error || !data) {
97+
console.error('Error creating tab database entry:', error);
98+
} else {
99+
this.pages.push(data.id);
100+
}
101+
102+
page?.on('close', () => this.handlePageClosed(data.id)());
103+
}
104+
105+
private handlePageClosed(tabId: string) {
106+
return async () => {
107+
const { error } = await supabase
108+
.from('browser_tabs')
109+
.update({ closed_at: new Date().toISOString() })
110+
.eq('id', tabId)
111+
.single();
112+
113+
if (error) {
114+
console.error('Error updating tab closed_at:', error);
115+
}
116+
117+
this.pages = this.pages.filter(id => id !== tabId);
118+
}
119+
}
120+
121+
private async handleUtilizationReport() {
122+
return await report([{
123+
type: 'custom.googleapis.com/browser_utilization',
124+
value: browsers.size / MAX_BROWSER_INSTANCES,
125+
}]);
126+
}
127+
}
128+
129+
/**
130+
* Handle SIGINT, SIGTERM, and SIGQUIT signals
131+
*/
132+
['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach(signal => {
133+
process.on(signal, () => {
134+
console.log(`${signal} received`);
135+
for (const browser of browsers.values()) {
136+
browser.close();
137+
}
138+
browsers.clear();
139+
process.exit(0);
140+
});
141+
});

src/constants.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const CHROME_SECURITY_FLAGS = [
2+
'--incognito', // Ensures fresh session
3+
'--disable-dev-shm-usage', // Prevents excessive memory use in /dev/shm
4+
'--renderer-process-limit=3', // Limits renderer processes (prevents high RAM usage)
5+
'--js-flags="--max_old_space_size=3072"', // Limits JavaScript heap memory to 3GB
6+
'--disable-background-networking', // Reduces background memory usage
7+
'--disable-sync', // Prevents syncing (reduces memory footprint)
8+
'--disable-extensions', // Prevents high-memory extension usage
9+
'--disable-password-generation', // Prevents password generation
10+
'--disable-save-password-bubble', // Prevents saving password bubble
11+
'--disable-autofill', // Prevents autofill
12+
'--disable-translate', // Prevents translation
13+
'--disable-ftp', // Prevents FTP
14+
];
15+
export const METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1';
16+
export const METADATA_HEADERS = { 'Metadata-Flavor': 'Google' };

src/environment.ts

-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ export const PORT = process.env.PORT || 3000;
44
export const SUPABASE_URL = process.env.SUPABASE_URL;
55
export const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
66
export const CHROME_ARGS = process.env.CHROME_ARGS?.split(' ') || [];
7-
export const API_KEY = process.env.API_KEY;
87
export const CHROME_PATH = process.env.CHROME_PATH;
98
export const MAX_BROWSER_INSTANCES = Math.max(os.cpus().length - 1, 1, parseInt(process.env.MAX_BROWSER_INSTANCES || '0'))
109
export const GCP_PROJECT_ID = process.env.GCP_PROJECT_ID;
@@ -21,7 +20,3 @@ if (SUPABASE_URL && SUPABASE_SERVICE_ROLE_KEY) {
2120
console.log('SUPABASE_URL:', SUPABASE_URL);
2221
console.log('SUPABASE_SERVICE_ROLE_KEY:', SUPABASE_SERVICE_ROLE_KEY.slice(0, 4) + '...');
2322
}
24-
25-
if (API_KEY) {
26-
console.log('API_KEY:', API_KEY.slice(0, 4) + '...');
27-
}

src/lib/supabase.ts

+20-23
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,32 @@
1-
import { createClient, SupabaseClient } from '@supabase/supabase-js';
1+
import { createClient } from '@supabase/supabase-js';
22
import * as env from '../environment';
33

4-
let supabase: SupabaseClient<any, "public", any> | undefined = undefined;
5-
6-
if (env.SUPABASE_URL && env.SUPABASE_SERVICE_ROLE_KEY) {
7-
supabase = createClient(
8-
env.SUPABASE_URL,
9-
env.SUPABASE_SERVICE_ROLE_KEY,
10-
{
11-
auth: {
12-
autoRefreshToken: false,
13-
persistSession: false,
14-
},
15-
}
16-
);
17-
} else if (!env.API_KEY) {
18-
console.error('Missing environment variables. API_KEY or SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required.');
19-
}
4+
if (!env.SUPABASE_URL || !env.SUPABASE_SERVICE_ROLE_KEY) {
5+
throw new Error('Missing environment variables. SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required.');
6+
}
7+
8+
export const supabase = createClient(
9+
env.SUPABASE_URL,
10+
env.SUPABASE_SERVICE_ROLE_KEY,
11+
{
12+
auth: {
13+
autoRefreshToken: false,
14+
persistSession: false,
15+
},
16+
}
17+
)
2018

21-
export async function validateApiKey(key: string): Promise<boolean> {
22-
if (key === env.API_KEY) return true;
23-
if (!supabase) return false;
19+
export async function getUser(token?: string | null): Promise<string | null> {
20+
if (!token) return null;
2421

2522
const { data, error } = await supabase
2623
.from('api_keys')
2724
.select('*')
28-
.eq('key', key)
25+
.eq('key', token)
2926
.single();
3027

3128
if (error || !data) {
32-
return false;
29+
return null;
3330
}
3431

3532
// Update last used timestamp
@@ -38,7 +35,7 @@ export async function validateApiKey(key: string): Promise<boolean> {
3835
.update({ last_used_at: new Date().toISOString() })
3936
.eq('id', data.id);
4037

41-
return true;
38+
return data.user;
4239
}
4340

4441
// export async function generateApiKey(userId: string): Promise<string | null> {

src/lib/systeminfo.ts

-40
This file was deleted.

0 commit comments

Comments
 (0)