chrome-control-proxy 1.0.0

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.
@@ -0,0 +1,182 @@
1
+ const { exec } = require('child_process');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const log = require('./logger');
5
+
6
+ const CHROME_PORT = process.env.CHROME_PORT || 9222;
7
+ const CHROME_PROFILE_DIR =
8
+ process.env.CHROME_PROFILE_DIR || path.join(os.homedir(), '.chrome-control-proxy', 'chrome-cdp');
9
+ const CHROME_BINARY =
10
+ process.env.CHROME_BINARY ||
11
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
12
+
13
+ function runCommand(command) {
14
+ return new Promise((resolve) => {
15
+ log.debug('browser', `exec: ${command.slice(0, 200)}${command.length > 200 ? '…' : ''}`);
16
+ exec(command, { shell: '/bin/bash' }, (error, stdout, stderr) => {
17
+ const out = {
18
+ ok: !error,
19
+ code: error ? error.code : 0,
20
+ stdout: stdout?.trim() || '',
21
+ stderr: stderr?.trim() || '',
22
+ };
23
+ if (!out.ok) {
24
+ log.debug('browser', `exec exit ${out.code}`, out.stderr || error?.message);
25
+ }
26
+ resolve(out);
27
+ });
28
+ });
29
+ }
30
+
31
+ async function getChromeStatus() {
32
+ const checkPortCmd = `lsof -nP -iTCP:${CHROME_PORT} -sTCP:LISTEN`;
33
+ const result = await runCommand(checkPortCmd);
34
+
35
+ if (!result.ok || !result.stdout) {
36
+ return {
37
+ running: false,
38
+ port: Number(CHROME_PORT),
39
+ pid: null,
40
+ raw: result.stdout || '',
41
+ };
42
+ }
43
+
44
+ const lines = result.stdout.split('\n').filter(Boolean);
45
+ const chromeLine = lines.find((line) => line.includes('Google Chrome') || line.includes('Chrome'));
46
+
47
+ if (!chromeLine) {
48
+ return {
49
+ running: true,
50
+ port: Number(CHROME_PORT),
51
+ pid: null,
52
+ raw: result.stdout,
53
+ };
54
+ }
55
+
56
+ const parts = chromeLine.trim().split(/\s+/);
57
+ const pid = parts[1] ? Number(parts[1]) : null;
58
+
59
+ return {
60
+ running: true,
61
+ port: Number(CHROME_PORT),
62
+ pid,
63
+ raw: chromeLine,
64
+ };
65
+ }
66
+
67
+ function buildStartChromeCommand() {
68
+ return [
69
+ 'nohup',
70
+ `"${CHROME_BINARY}"`,
71
+ `--remote-debugging-port=${CHROME_PORT}`,
72
+ `--user-data-dir="${CHROME_PROFILE_DIR}"`,
73
+ '--lang=zh-CN',
74
+ '--disable-dev-shm-usage',
75
+ '--no-first-run',
76
+ '--no-default-browser-check',
77
+ '>/tmp/chrome-cdp.log 2>&1 &',
78
+ ].join(' ');
79
+ }
80
+
81
+ async function startChrome() {
82
+ log.info('browser', 'startChrome requested');
83
+ const status = await getChromeStatus();
84
+ if (status.running) {
85
+ log.info('browser', 'Chrome already running', { port: CHROME_PORT });
86
+ return {
87
+ changed: false,
88
+ message: 'Chrome is already running',
89
+ status,
90
+ };
91
+ }
92
+
93
+ await runCommand(`mkdir -p "${CHROME_PROFILE_DIR}"`);
94
+ const cmd = buildStartChromeCommand();
95
+ log.info('browser', 'launching Chrome', { port: CHROME_PORT, profile: CHROME_PROFILE_DIR });
96
+ const result = await runCommand(cmd);
97
+
98
+ await new Promise((r) => setTimeout(r, 15000));
99
+ const nextStatus = await getChromeStatus();
100
+ log.info('browser', 'startChrome done', { running: nextStatus.running, commandOk: result.ok });
101
+
102
+ return {
103
+ changed: true,
104
+ message: nextStatus.running
105
+ ? 'Chrome started successfully'
106
+ : 'Chrome start command executed, but browser is not confirmed running yet',
107
+ commandOk: result.ok,
108
+ status: nextStatus,
109
+ stderr: result.stderr,
110
+ };
111
+ }
112
+
113
+ function resetPlaywrightConnectionLazy() {
114
+ log.info('browser', 'reset Playwright CDP connection (Chrome stopped)');
115
+ require('./playwright-controller').resetPlaywrightConnection();
116
+ }
117
+
118
+ async function stopChrome() {
119
+ log.info('browser', 'stopChrome requested');
120
+ const status = await getChromeStatus();
121
+ if (!status.running) {
122
+ log.info('browser', 'Chrome not running, skip stop');
123
+ return {
124
+ changed: false,
125
+ message: 'Chrome is not running',
126
+ status,
127
+ };
128
+ }
129
+
130
+ let result;
131
+ if (status.pid) {
132
+ log.info('browser', `sending SIGTERM to Chrome pid ${status.pid}`);
133
+ result = await runCommand(`kill ${status.pid}`);
134
+ await new Promise((r) => setTimeout(r, 1000));
135
+ } else {
136
+ log.info('browser', 'no pid, using pkill pattern for Chrome');
137
+ result = await runCommand(`pkill -f "Google Chrome.*--remote-debugging-port=${CHROME_PORT}"`);
138
+ await new Promise((r) => setTimeout(r, 1000));
139
+ }
140
+
141
+ const nextStatus = await getChromeStatus();
142
+ log.info('browser', 'stopChrome done', { stillRunning: nextStatus.running });
143
+
144
+ if (!nextStatus.running) {
145
+ resetPlaywrightConnectionLazy();
146
+ }
147
+
148
+ return {
149
+ changed: true,
150
+ message: nextStatus.running
151
+ ? 'Chrome stop command executed, but browser still appears to be running'
152
+ : 'Chrome stopped successfully',
153
+ commandOk: result.ok,
154
+ status: nextStatus,
155
+ stderr: result.stderr,
156
+ };
157
+ }
158
+
159
+ async function restartChrome() {
160
+ log.info('browser', 'restartChrome requested');
161
+ const stopResult = await stopChrome();
162
+ await new Promise((r) => setTimeout(r, 1200));
163
+ const startResult = await startChrome();
164
+ log.info('browser', 'restartChrome completed');
165
+
166
+ return {
167
+ changed: true,
168
+ message: 'Chrome restart completed',
169
+ stop: stopResult,
170
+ start: startResult,
171
+ };
172
+ }
173
+
174
+ module.exports = {
175
+ CHROME_PORT,
176
+ CHROME_PROFILE_DIR,
177
+ CHROME_BINARY,
178
+ getChromeStatus,
179
+ startChrome,
180
+ stopChrome,
181
+ restartChrome,
182
+ };
package/lib/logger.js ADDED
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const LEVELS = { error: 0, warn: 1, info: 2, debug: 3 };
7
+ const LOG_LEVEL = (process.env.LOG_LEVEL || 'info').toLowerCase();
8
+ const threshold = LEVELS[LOG_LEVEL] !== undefined ? LEVELS[LOG_LEVEL] : 2;
9
+
10
+ const PREFIX = '[chrome-control-proxy]';
11
+ const LOG_DIR = process.env.LOG_DIR || '';
12
+ const LOG_CONSOLE = !/^(0|false|no)$/i.test(String(process.env.LOG_CONSOLE ?? 'true'));
13
+ const LOG_MAX_FILE_MB = Math.max(0, Number(process.env.LOG_MAX_FILE_MB) || 0);
14
+ const LOG_MAX_BYTES = LOG_MAX_FILE_MB > 0 ? Math.floor(LOG_MAX_FILE_MB * 1024 * 1024) : 0;
15
+
16
+ let fileStream = null;
17
+ let streamDateKey = null;
18
+ let streamPart = 0;
19
+ let bytesInCurrentFile = 0;
20
+
21
+ function ts() {
22
+ return new Date().toISOString();
23
+ }
24
+
25
+ function dateKey() {
26
+ return new Date().toISOString().slice(0, 10);
27
+ }
28
+
29
+ function formatErr(e) {
30
+ if (e === undefined || e === null) {
31
+ return '';
32
+ }
33
+ if (e instanceof Error) {
34
+ return `${e.message}\n${e.stack || ''}`;
35
+ }
36
+ if (typeof e === 'object') {
37
+ try {
38
+ return JSON.stringify(e);
39
+ } catch {
40
+ return String(e);
41
+ }
42
+ }
43
+ return String(e);
44
+ }
45
+
46
+ function fmtMeta(meta) {
47
+ if (meta === undefined || meta === null) {
48
+ return '';
49
+ }
50
+ if (typeof meta === 'object' && !(meta instanceof Error)) {
51
+ try {
52
+ return JSON.stringify(meta);
53
+ } catch {
54
+ return String(meta);
55
+ }
56
+ }
57
+ return String(meta);
58
+ }
59
+
60
+ function buildFileText(level, ns, msg, extra) {
61
+ let text = `${PREFIX} ${ts()} [${level}] [${ns}] ${msg}`;
62
+ if (extra !== undefined && extra !== '') {
63
+ if (level === 'error') {
64
+ text += `\n${formatErr(extra)}`;
65
+ } else {
66
+ text += ` ${fmtMeta(extra)}`;
67
+ }
68
+ }
69
+ return `${text}\n`;
70
+ }
71
+
72
+ function fileNameForPart(dk, part) {
73
+ return part === 0
74
+ ? `ccp-${dk}.log`
75
+ : `ccp-${dk}.${part}.log`;
76
+ }
77
+
78
+ function openNewStream(dk, part) {
79
+ fs.mkdirSync(LOG_DIR, { recursive: true });
80
+ const fullPath = path.join(LOG_DIR, fileNameForPart(dk, part));
81
+ return fs.createWriteStream(fullPath, { flags: 'a' });
82
+ }
83
+
84
+ function closeFileStreamSync() {
85
+ if (!fileStream) {
86
+ return;
87
+ }
88
+ try {
89
+ fileStream.end();
90
+ } catch (_) {
91
+ /* */
92
+ }
93
+ fileStream = null;
94
+ }
95
+
96
+ function closeFileStream() {
97
+ return new Promise((resolve) => {
98
+ if (!fileStream) {
99
+ resolve();
100
+ return;
101
+ }
102
+ const s = fileStream;
103
+ fileStream = null;
104
+ streamDateKey = null;
105
+ streamPart = 0;
106
+ bytesInCurrentFile = 0;
107
+ s.end(() => resolve());
108
+ });
109
+ }
110
+
111
+ function ensureFileStreamBeforeWrite(chunkByteLength) {
112
+ const dk = dateKey();
113
+
114
+ if (!fileStream) {
115
+ streamDateKey = dk;
116
+ streamPart = 0;
117
+ bytesInCurrentFile = 0;
118
+ fileStream = openNewStream(streamDateKey, streamPart);
119
+ return;
120
+ }
121
+
122
+ if (streamDateKey !== dk) {
123
+ closeFileStreamSync();
124
+ streamDateKey = dk;
125
+ streamPart = 0;
126
+ bytesInCurrentFile = 0;
127
+ fileStream = openNewStream(streamDateKey, streamPart);
128
+ return;
129
+ }
130
+
131
+ if (
132
+ LOG_MAX_BYTES > 0 &&
133
+ bytesInCurrentFile + chunkByteLength > LOG_MAX_BYTES &&
134
+ bytesInCurrentFile > 0
135
+ ) {
136
+ closeFileStreamSync();
137
+ streamPart += 1;
138
+ bytesInCurrentFile = 0;
139
+ fileStream = openNewStream(streamDateKey, streamPart);
140
+ }
141
+ }
142
+
143
+ function writeFileLog(level, ns, msg, extra) {
144
+ if (!LOG_DIR) {
145
+ return;
146
+ }
147
+ const chunk = buildFileText(level, ns, msg, extra);
148
+ const bufLen = Buffer.byteLength(chunk, 'utf8');
149
+
150
+ try {
151
+ ensureFileStreamBeforeWrite(bufLen);
152
+ if (fileStream) {
153
+ fileStream.write(chunk);
154
+ bytesInCurrentFile += bufLen;
155
+ }
156
+ } catch (e) {
157
+ console.error(`${PREFIX} logger file sink error`, e);
158
+ }
159
+ }
160
+
161
+ function emit(level, ns, msg, extra) {
162
+ if (LEVELS[level] > threshold) {
163
+ return;
164
+ }
165
+ const line = `${PREFIX} ${ts()} [${level}] [${ns}] ${msg}`;
166
+ if (LOG_CONSOLE) {
167
+ if (level === 'error') {
168
+ console.error(line, extra !== undefined && extra !== '' ? formatErr(extra) : '');
169
+ } else if (level === 'warn') {
170
+ console.warn(line, extra !== undefined ? fmtMeta(extra) : '');
171
+ } else {
172
+ console.log(line, extra !== undefined ? fmtMeta(extra) : '');
173
+ }
174
+ }
175
+ writeFileLog(level, ns, msg, extra);
176
+ }
177
+
178
+ module.exports = {
179
+ error(ns, msg, err) {
180
+ emit('error', ns, msg, err);
181
+ },
182
+ warn(ns, msg, meta) {
183
+ emit('warn', ns, msg, meta);
184
+ },
185
+ info(ns, msg, meta) {
186
+ emit('info', ns, msg, meta);
187
+ },
188
+ debug(ns, msg, meta) {
189
+ emit('debug', ns, msg, meta);
190
+ },
191
+ closeFileSink: closeFileStream,
192
+ };