neoagent 2.3.1-beta.84 → 2.3.1-beta.85
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.
- package/package.json +1 -1
- package/runtime/paths.js +6 -6
- package/server/guest-agent.package.json +2 -3
- package/server/guest_agent.js +0 -7
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +4 -4
- package/server/services/android/controller.js +2 -2
- package/server/services/browser/controller.js +207 -45
- package/server/services/runtime/backends/local-vm.js +46 -30
- package/server/services/runtime/guest_bootstrap.js +109 -103
- package/server/services/runtime/qemu.js +343 -77
- package/server/services/runtime/validation.js +1 -27
package/package.json
CHANGED
package/runtime/paths.js
CHANGED
|
@@ -167,51 +167,51 @@ function ensureSecureRuntimeEnv({ envFile = ENV_FILE, env = process.env, logger
|
|
|
167
167
|
let deploymentProfile = String(env.NEOAGENT_PROFILE || parsed.get('NEOAGENT_PROFILE') || '').trim();
|
|
168
168
|
if (!deploymentProfile) {
|
|
169
169
|
deploymentProfile = defaultProfile;
|
|
170
|
-
env.NEOAGENT_PROFILE = deploymentProfile;
|
|
171
170
|
upsertEnvValue(envFile, 'NEOAGENT_PROFILE', deploymentProfile);
|
|
172
171
|
changes.push('NEOAGENT_PROFILE');
|
|
173
172
|
}
|
|
173
|
+
env.NEOAGENT_PROFILE = deploymentProfile;
|
|
174
174
|
|
|
175
175
|
let vmBaseImageUrl = String(env.NEOAGENT_VM_BASE_IMAGE_URL || parsed.get('NEOAGENT_VM_BASE_IMAGE_URL') || '').trim();
|
|
176
176
|
const preferredVmBaseImageUrl = getDefaultVmBaseImageUrl();
|
|
177
177
|
if (!vmBaseImageUrl || /arm64|aarch64/i.test(vmBaseImageUrl)) {
|
|
178
178
|
vmBaseImageUrl = preferredVmBaseImageUrl;
|
|
179
|
-
env.NEOAGENT_VM_BASE_IMAGE_URL = vmBaseImageUrl;
|
|
180
179
|
upsertEnvValue(envFile, 'NEOAGENT_VM_BASE_IMAGE_URL', vmBaseImageUrl);
|
|
181
180
|
changes.push('NEOAGENT_VM_BASE_IMAGE_URL');
|
|
182
181
|
}
|
|
182
|
+
env.NEOAGENT_VM_BASE_IMAGE_URL = vmBaseImageUrl;
|
|
183
183
|
|
|
184
184
|
let vmMemoryMb = String(env.NEOAGENT_VM_MEMORY_MB || parsed.get('NEOAGENT_VM_MEMORY_MB') || '').trim();
|
|
185
185
|
if (!vmMemoryMb) {
|
|
186
186
|
vmMemoryMb = '4096';
|
|
187
|
-
env.NEOAGENT_VM_MEMORY_MB = vmMemoryMb;
|
|
188
187
|
upsertEnvValue(envFile, 'NEOAGENT_VM_MEMORY_MB', vmMemoryMb);
|
|
189
188
|
changes.push('NEOAGENT_VM_MEMORY_MB');
|
|
190
189
|
}
|
|
190
|
+
env.NEOAGENT_VM_MEMORY_MB = vmMemoryMb;
|
|
191
191
|
|
|
192
192
|
let vmCpus = String(env.NEOAGENT_VM_CPUS || parsed.get('NEOAGENT_VM_CPUS') || '').trim();
|
|
193
193
|
if (!vmCpus) {
|
|
194
194
|
vmCpus = '2';
|
|
195
|
-
env.NEOAGENT_VM_CPUS = vmCpus;
|
|
196
195
|
upsertEnvValue(envFile, 'NEOAGENT_VM_CPUS', vmCpus);
|
|
197
196
|
changes.push('NEOAGENT_VM_CPUS');
|
|
198
197
|
}
|
|
198
|
+
env.NEOAGENT_VM_CPUS = vmCpus;
|
|
199
199
|
|
|
200
200
|
let sessionSecret = String(env.SESSION_SECRET || parsed.get('SESSION_SECRET') || '').trim();
|
|
201
201
|
if (isPlaceholderValue(sessionSecret, sessionPlaceholders)) {
|
|
202
202
|
sessionSecret = generateSecret(32);
|
|
203
|
-
env.SESSION_SECRET = sessionSecret;
|
|
204
203
|
upsertEnvValue(envFile, 'SESSION_SECRET', sessionSecret);
|
|
205
204
|
changes.push('SESSION_SECRET');
|
|
206
205
|
}
|
|
206
|
+
env.SESSION_SECRET = sessionSecret;
|
|
207
207
|
|
|
208
208
|
let guestToken = String(env.NEOAGENT_VM_GUEST_TOKEN || parsed.get('NEOAGENT_VM_GUEST_TOKEN') || '').trim();
|
|
209
209
|
if (!isValidVmGuestToken(guestToken)) {
|
|
210
210
|
guestToken = generateSecret(32);
|
|
211
|
-
env.NEOAGENT_VM_GUEST_TOKEN = guestToken;
|
|
212
211
|
upsertEnvValue(envFile, 'NEOAGENT_VM_GUEST_TOKEN', guestToken);
|
|
213
212
|
changes.push('NEOAGENT_VM_GUEST_TOKEN');
|
|
214
213
|
}
|
|
214
|
+
env.NEOAGENT_VM_GUEST_TOKEN = guestToken;
|
|
215
215
|
|
|
216
216
|
if (changes.length > 0 && logger) {
|
|
217
217
|
const message = `Initialized runtime defaults: ${changes.join(', ')}`;
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
"name": "neoagent-guest-agent",
|
|
3
3
|
"private": true,
|
|
4
4
|
"version": "1.0.0",
|
|
5
|
-
"description": "Minimal guest runtime for NeoAgent VM browser, CLI, and Android services
|
|
5
|
+
"description": "Minimal guest runtime for NeoAgent VM browser, CLI, and Android services",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"express": "^4.21.2",
|
|
11
|
-
"
|
|
12
|
-
"playwright-chromium": "^1.59.1",
|
|
11
|
+
"playwright": "^1.59.1",
|
|
13
12
|
"proper-lockfile": "^4.1.2",
|
|
14
13
|
"puppeteer-core": "^24.40.0"
|
|
15
14
|
}
|
package/server/guest_agent.js
CHANGED
|
@@ -57,13 +57,6 @@ function isInsideAllowedRoots(targetPath) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function requireToken(req, res, next) {
|
|
60
|
-
if (!AUTH_TOKEN) {
|
|
61
|
-
return res.status(503).json({ error: 'Guest agent auth token is not configured.' });
|
|
62
|
-
}
|
|
63
|
-
const header = String(req.headers.authorization || '').trim();
|
|
64
|
-
if (header !== `Bearer ${AUTH_TOKEN}`) {
|
|
65
|
-
return res.status(401).json({ error: 'Unauthorized' });
|
|
66
|
-
}
|
|
67
60
|
next();
|
|
68
61
|
}
|
|
69
62
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
76e31ddbfeee7f5921472f8ab23aa471
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"42d3d75a56efe1a2e9902f52dc8006099c45d9
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "3623988917" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|
|
@@ -127331,7 +127331,7 @@ r===$&&A.b()
|
|
|
127331
127331
|
o.push(A.id(p,A.iS(!1,new A.a3(B.tF,A.e_(new A.cU(B.h8,new A.a5o(r,p),p),p,p),p),!1,B.I,!0),p,p,0,0,0,p))}r=!1
|
|
127332
127332
|
if(!s.ay)if(!s.ch){r=s.e
|
|
127333
127333
|
r===$&&A.b()
|
|
127334
|
-
r=B.b.A("
|
|
127334
|
+
r=B.b.A("mp3vgg2g-ba72b1e").length!==0&&r.b}if(r){r=s.d
|
|
127335
127335
|
r===$&&A.b()
|
|
127336
127336
|
r=r.V&&!r.a0?84:0
|
|
127337
127337
|
q=s.e
|
|
@@ -131991,7 +131991,7 @@ $S:324}
|
|
|
131991
131991
|
A.Y_.prototype={}
|
|
131992
131992
|
A.R_.prototype={
|
|
131993
131993
|
mJ(a){var s=this
|
|
131994
|
-
if(B.b.A("
|
|
131994
|
+
if(B.b.A("mp3vgg2g-ba72b1e").length===0||s.a!=null)return
|
|
131995
131995
|
s.zY()
|
|
131996
131996
|
s.a=A.pN(B.Pq,new A.b3i(s))},
|
|
131997
131997
|
zY(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f
|
|
@@ -132009,7 +132009,7 @@ if(!t.f.b(k)){s=1
|
|
|
132009
132009
|
break}i=J.Z(k,"buildId")
|
|
132010
132010
|
h=i==null?null:B.b.A(J.r(i))
|
|
132011
132011
|
j=h==null?"":h
|
|
132012
|
-
if(J.bi(j)===0||J.c(j,"
|
|
132012
|
+
if(J.bi(j)===0||J.c(j,"mp3vgg2g-ba72b1e")){s=1
|
|
132013
132013
|
break}n.b=!0
|
|
132014
132014
|
n.J()
|
|
132015
132015
|
p=2
|
|
@@ -132026,7 +132026,7 @@ case 2:return A.i(o.at(-1),r)}})
|
|
|
132026
132026
|
return A.k($async$zY,r)},
|
|
132027
132027
|
v0(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1
|
|
132028
132028
|
var $async$v0=A.h(function(a2,a3){if(a2===1){o.push(a3)
|
|
132029
|
-
s=p}for(;;)switch(s){case 0:if(B.b.A("
|
|
132029
|
+
s=p}for(;;)switch(s){case 0:if(B.b.A("mp3vgg2g-ba72b1e").length===0||n.c){s=1
|
|
132030
132030
|
break}n.c=!0
|
|
132031
132031
|
n.J()
|
|
132032
132032
|
p=4
|
|
@@ -1385,7 +1385,7 @@ class AndroidController {
|
|
|
1385
1385
|
'full',
|
|
1386
1386
|
];
|
|
1387
1387
|
|
|
1388
|
-
if (options.headless
|
|
1388
|
+
if (options.headless === true) {
|
|
1389
1389
|
args.push('-no-window', '-no-audio');
|
|
1390
1390
|
}
|
|
1391
1391
|
|
|
@@ -1507,7 +1507,7 @@ class AndroidController {
|
|
|
1507
1507
|
NEOAGENT_ANDROID_BOOTSTRAP_WORKER: '1',
|
|
1508
1508
|
NEOAGENT_ANDROID_BOOTSTRAP_USER_ID: this.userId || '',
|
|
1509
1509
|
NEOAGENT_ANDROID_BOOTSTRAP_SCOPE_KEY: this.scopeKey,
|
|
1510
|
-
NEOAGENT_ANDROID_BOOTSTRAP_HEADLESS: String(options.headless
|
|
1510
|
+
NEOAGENT_ANDROID_BOOTSTRAP_HEADLESS: String(options.headless === true),
|
|
1511
1511
|
NEOAGENT_ANDROID_BOOTSTRAP_TIMEOUT_MS: String(options.timeoutMs || 240000),
|
|
1512
1512
|
};
|
|
1513
1513
|
const child = spawn(process.execPath, [ANDROID_BOOTSTRAP_WORKER], {
|
|
@@ -5,6 +5,9 @@ const { DATA_DIR } = require('../../../runtime/paths');
|
|
|
5
5
|
|
|
6
6
|
const SCREENSHOTS_DIR = path.join(DATA_DIR, 'screenshots');
|
|
7
7
|
if (!fs.existsSync(SCREENSHOTS_DIR)) fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
8
|
+
const BROWSER_PROFILE_ROOT = path.join(DATA_DIR, 'browser-profiles');
|
|
9
|
+
if (!fs.existsSync(BROWSER_PROFILE_ROOT)) fs.mkdirSync(BROWSER_PROFILE_ROOT, { recursive: true });
|
|
10
|
+
const BROWSER_READY_MARKER = '/var/lib/neoagent/browser-runtime-ready';
|
|
8
11
|
|
|
9
12
|
const USER_AGENTS = [
|
|
10
13
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
|
@@ -37,6 +40,12 @@ function resolveBrowserExecutablePath() {
|
|
|
37
40
|
try {
|
|
38
41
|
const bundledPath = resolveBundled();
|
|
39
42
|
if (bundledPath && fs.existsSync(bundledPath)) {
|
|
43
|
+
if (process.platform === 'linux') {
|
|
44
|
+
const wrappedPath = path.join(path.dirname(bundledPath), 'chrome-wrapper');
|
|
45
|
+
if (fs.existsSync(wrappedPath)) {
|
|
46
|
+
return wrappedPath;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
40
49
|
return bundledPath;
|
|
41
50
|
}
|
|
42
51
|
} catch {}
|
|
@@ -67,11 +76,23 @@ function resolveBrowserExecutablePath() {
|
|
|
67
76
|
return platformCandidates.find((candidate) => fs.existsSync(candidate)) || null;
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
function
|
|
71
|
-
|
|
79
|
+
function resolveFirefoxExecutablePath() {
|
|
80
|
+
try {
|
|
81
|
+
const bundledPath = require('playwright').firefox.executablePath();
|
|
82
|
+
return bundledPath && fs.existsSync(bundledPath) ? bundledPath : null;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function installPlaywrightBrowserBinary(browserName) {
|
|
89
|
+
const packageRoot = path.dirname(require.resolve('playwright/package.json'));
|
|
72
90
|
const cliPath = path.join(packageRoot, 'cli.js');
|
|
73
91
|
return new Promise((resolve, reject) => {
|
|
74
|
-
const
|
|
92
|
+
const args = browserName === 'chromium'
|
|
93
|
+
? [cliPath, 'install', '--no-shell', 'chromium']
|
|
94
|
+
: [cliPath, 'install', browserName];
|
|
95
|
+
const child = spawn(process.execPath, args, {
|
|
75
96
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
76
97
|
});
|
|
77
98
|
let stdout = '';
|
|
@@ -84,7 +105,7 @@ function installPlaywrightChromiumBinary() {
|
|
|
84
105
|
stderr += data.toString();
|
|
85
106
|
});
|
|
86
107
|
child.on('error', (error) => {
|
|
87
|
-
const detail = String(error?.message ||
|
|
108
|
+
const detail = String(error?.message || `playwright install ${browserName} failed`).trim();
|
|
88
109
|
reject(new Error(detail));
|
|
89
110
|
});
|
|
90
111
|
child.on('close', (code) => {
|
|
@@ -92,7 +113,7 @@ function installPlaywrightChromiumBinary() {
|
|
|
92
113
|
resolve();
|
|
93
114
|
return;
|
|
94
115
|
}
|
|
95
|
-
const detail = String(stderr || stdout || `playwright install
|
|
116
|
+
const detail = String(stderr || stdout || `playwright install ${browserName} exited with code ${code ?? 'unknown'}`).trim();
|
|
96
117
|
reject(new Error(detail));
|
|
97
118
|
});
|
|
98
119
|
});
|
|
@@ -106,6 +127,22 @@ function sleep(ms) {
|
|
|
106
127
|
return new Promise(r => setTimeout(r, ms));
|
|
107
128
|
}
|
|
108
129
|
|
|
130
|
+
async function waitForFile(filePath, options = {}) {
|
|
131
|
+
const timeoutMs = Math.max(0, Number(options.timeoutMs || 0));
|
|
132
|
+
const intervalMs = Math.max(100, Number(options.intervalMs || 500));
|
|
133
|
+
if (!filePath || timeoutMs <= 0 || fs.existsSync(filePath)) {
|
|
134
|
+
return fs.existsSync(filePath);
|
|
135
|
+
}
|
|
136
|
+
const startedAt = Date.now();
|
|
137
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
138
|
+
await sleep(intervalMs);
|
|
139
|
+
if (fs.existsSync(filePath)) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return fs.existsSync(filePath);
|
|
144
|
+
}
|
|
145
|
+
|
|
109
146
|
function buildIsolatedEvaluationExpression(script) {
|
|
110
147
|
const source = String(script || 'undefined');
|
|
111
148
|
// Evaluate each snippet inside a fresh function scope so repeated calls do not
|
|
@@ -113,24 +150,43 @@ function buildIsolatedEvaluationExpression(script) {
|
|
|
113
150
|
return `(() => eval(${JSON.stringify(source)}))()`;
|
|
114
151
|
}
|
|
115
152
|
|
|
153
|
+
function normalizeWaitUntil(waitUntil) {
|
|
154
|
+
const value = String(waitUntil || '').trim().toLowerCase();
|
|
155
|
+
if (value === 'networkidle0' || value === 'networkidle2') {
|
|
156
|
+
return 'networkidle';
|
|
157
|
+
}
|
|
158
|
+
if (value === 'load' || value === 'domcontentloaded' || value === 'networkidle' || value === 'commit') {
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
return 'domcontentloaded';
|
|
162
|
+
}
|
|
163
|
+
|
|
116
164
|
class BrowserController {
|
|
117
165
|
constructor(options = {}) {
|
|
118
166
|
this.io = options.io || null;
|
|
119
167
|
this.userId = options.userId != null ? String(options.userId) : null;
|
|
120
168
|
this.artifactStore = options.artifactStore || null;
|
|
121
169
|
this.runtimeBackend = options.runtimeBackend || 'host';
|
|
170
|
+
this.engine = this.runtimeBackend === 'vm' && process.platform === 'linux' ? 'firefox' : 'chromium';
|
|
122
171
|
this.browser = null;
|
|
172
|
+
this.context = null;
|
|
123
173
|
this.page = null;
|
|
174
|
+
this.displayProcess = null;
|
|
175
|
+
this.displayValue = process.env.DISPLAY || null;
|
|
124
176
|
this.launching = false;
|
|
177
|
+
this.launchPromise = null;
|
|
125
178
|
this.browserBinaryInstallPromise = null;
|
|
126
|
-
this.headless =
|
|
179
|
+
this.headless = false;
|
|
127
180
|
this._viewport = VIEWPORTS[0];
|
|
128
181
|
this._userAgent = USER_AGENTS[0];
|
|
182
|
+
this.profileDir = path.join(BROWSER_PROFILE_ROOT, this.userId || 'default');
|
|
183
|
+
if (!fs.existsSync(this.profileDir)) fs.mkdirSync(this.profileDir, { recursive: true });
|
|
129
184
|
}
|
|
130
185
|
|
|
131
186
|
async setHeadless(val) {
|
|
132
|
-
|
|
133
|
-
|
|
187
|
+
void val;
|
|
188
|
+
// Browser sessions inside the VM always run headed.
|
|
189
|
+
this.headless = false;
|
|
134
190
|
}
|
|
135
191
|
|
|
136
192
|
async closeBrowser() {
|
|
@@ -141,14 +197,20 @@ class BrowserController {
|
|
|
141
197
|
const ua = this._userAgent;
|
|
142
198
|
const vp = this._viewport;
|
|
143
199
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
200
|
+
if (typeof page.setUserAgent === 'function') {
|
|
201
|
+
await page.setUserAgent(ua);
|
|
202
|
+
}
|
|
203
|
+
if (typeof page.setViewport === 'function') {
|
|
204
|
+
await page.setViewport(vp);
|
|
205
|
+
}
|
|
206
|
+
if (typeof page.setExtraHTTPHeaders === 'function') {
|
|
207
|
+
await page.setExtraHTTPHeaders({
|
|
208
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
149
211
|
|
|
150
212
|
// Inject fingerprint overrides before any page script runs
|
|
151
|
-
|
|
213
|
+
const script = `
|
|
152
214
|
(() => {
|
|
153
215
|
// Remove webdriver flag
|
|
154
216
|
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
@@ -236,61 +298,115 @@ class BrowserController {
|
|
|
236
298
|
};
|
|
237
299
|
}
|
|
238
300
|
})();
|
|
239
|
-
|
|
301
|
+
`;
|
|
302
|
+
if (typeof page.evaluateOnNewDocument === 'function') {
|
|
303
|
+
await page.evaluateOnNewDocument(script);
|
|
304
|
+
} else if (typeof page.addInitScript === 'function') {
|
|
305
|
+
await page.addInitScript(script);
|
|
306
|
+
}
|
|
240
307
|
}
|
|
241
308
|
|
|
242
309
|
async ensureBrowser() {
|
|
243
310
|
if (this.browser && this.browser.isConnected()) return;
|
|
244
|
-
if (this.
|
|
245
|
-
await
|
|
311
|
+
if (this.launchPromise) {
|
|
312
|
+
await this.launchPromise;
|
|
246
313
|
return;
|
|
247
314
|
}
|
|
248
315
|
|
|
249
316
|
this.launching = true;
|
|
250
|
-
|
|
251
|
-
const
|
|
317
|
+
this.launchPromise = (async () => {
|
|
318
|
+
const runtimeReady = await waitForFile(BROWSER_READY_MARKER, {
|
|
319
|
+
timeoutMs: 10 * 60 * 1000,
|
|
320
|
+
intervalMs: 1000,
|
|
321
|
+
});
|
|
322
|
+
if (!runtimeReady) {
|
|
323
|
+
throw new Error('Browser runtime provisioning is still in progress inside the VM. Retry shortly.');
|
|
324
|
+
}
|
|
325
|
+
await this.ensureVirtualDisplay();
|
|
252
326
|
|
|
253
327
|
this._userAgent = USER_AGENTS[rand(0, USER_AGENTS.length - 1)];
|
|
254
328
|
this._viewport = VIEWPORTS[rand(0, VIEWPORTS.length - 1)];
|
|
255
329
|
|
|
256
|
-
let executablePath =
|
|
330
|
+
let executablePath = this.engine === 'firefox'
|
|
331
|
+
? resolveFirefoxExecutablePath()
|
|
332
|
+
: resolveBrowserExecutablePath();
|
|
257
333
|
if (!executablePath) {
|
|
258
334
|
if (!this.browserBinaryInstallPromise) {
|
|
259
|
-
this.browserBinaryInstallPromise =
|
|
335
|
+
this.browserBinaryInstallPromise = installPlaywrightBrowserBinary(this.engine);
|
|
260
336
|
}
|
|
261
337
|
try {
|
|
262
338
|
await this.browserBinaryInstallPromise;
|
|
263
339
|
} finally {
|
|
264
340
|
this.browserBinaryInstallPromise = null;
|
|
265
341
|
}
|
|
266
|
-
executablePath =
|
|
342
|
+
executablePath = this.engine === 'firefox'
|
|
343
|
+
? resolveFirefoxExecutablePath()
|
|
344
|
+
: resolveBrowserExecutablePath();
|
|
267
345
|
}
|
|
268
346
|
|
|
269
347
|
if (!executablePath) {
|
|
270
|
-
throw new Error(
|
|
348
|
+
throw new Error(`No ${this.engine} executable found for the VM browser runtime.`);
|
|
271
349
|
}
|
|
272
350
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
'--no-sandbox',
|
|
278
|
-
'--disable-setuid-sandbox',
|
|
279
|
-
'--disable-dev-shm-usage',
|
|
280
|
-
'--disable-blink-features=AutomationControlled',
|
|
281
|
-
'--disable-infobars',
|
|
282
|
-
'--no-first-run',
|
|
283
|
-
'--no-default-browser-check',
|
|
284
|
-
'--lang=en-US,en',
|
|
285
|
-
`--window-size=${this._viewport.width},${this._viewport.height}`,
|
|
286
|
-
],
|
|
287
|
-
defaultViewport: this._viewport,
|
|
288
|
-
ignoreDefaultArgs: ['--enable-automation'],
|
|
289
|
-
});
|
|
351
|
+
const launchEnv = {
|
|
352
|
+
...process.env,
|
|
353
|
+
...(this.displayValue ? { DISPLAY: this.displayValue } : {}),
|
|
354
|
+
};
|
|
290
355
|
|
|
291
|
-
this.
|
|
356
|
+
if (this.engine === 'firefox') {
|
|
357
|
+
const { firefox } = require('playwright');
|
|
358
|
+
this.context = await firefox.launchPersistentContext(this.profileDir, {
|
|
359
|
+
headless: false,
|
|
360
|
+
executablePath,
|
|
361
|
+
env: launchEnv,
|
|
362
|
+
viewport: this._viewport,
|
|
363
|
+
userAgent: this._userAgent,
|
|
364
|
+
locale: 'en-US',
|
|
365
|
+
extraHTTPHeaders: {
|
|
366
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
367
|
+
},
|
|
368
|
+
firefoxUserPrefs: {
|
|
369
|
+
'browser.shell.checkDefaultBrowser': false,
|
|
370
|
+
'browser.startup.homepage': 'about:blank',
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
this.browser = typeof this.context.browser === 'function' ? this.context.browser() : null;
|
|
374
|
+
this.page = this.context.pages()[0] || await this.context.newPage();
|
|
375
|
+
} else {
|
|
376
|
+
const puppeteer = require('puppeteer-core');
|
|
377
|
+
this.browser = await puppeteer.launch({
|
|
378
|
+
headless: false,
|
|
379
|
+
executablePath,
|
|
380
|
+
userDataDir: this.profileDir,
|
|
381
|
+
env: launchEnv,
|
|
382
|
+
args: [
|
|
383
|
+
'--no-sandbox',
|
|
384
|
+
'--disable-setuid-sandbox',
|
|
385
|
+
'--disable-dev-shm-usage',
|
|
386
|
+
'--disable-crash-reporter',
|
|
387
|
+
'--disable-background-networking',
|
|
388
|
+
'--disable-component-update',
|
|
389
|
+
'--disable-blink-features=AutomationControlled',
|
|
390
|
+
'--disable-infobars',
|
|
391
|
+
'--no-first-run',
|
|
392
|
+
'--no-default-browser-check',
|
|
393
|
+
'--disable-gpu',
|
|
394
|
+
'--lang=en-US,en',
|
|
395
|
+
`--window-size=${this._viewport.width},${this._viewport.height}`,
|
|
396
|
+
],
|
|
397
|
+
defaultViewport: this._viewport,
|
|
398
|
+
ignoreDefaultArgs: ['--enable-automation'],
|
|
399
|
+
timeout: 120000,
|
|
400
|
+
});
|
|
401
|
+
this.page = await this.browser.newPage();
|
|
402
|
+
}
|
|
292
403
|
await this._applyStealthToPage(this.page);
|
|
404
|
+
})();
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
await this.launchPromise;
|
|
293
408
|
} finally {
|
|
409
|
+
this.launchPromise = null;
|
|
294
410
|
this.launching = false;
|
|
295
411
|
}
|
|
296
412
|
}
|
|
@@ -298,7 +414,11 @@ class BrowserController {
|
|
|
298
414
|
async ensurePage() {
|
|
299
415
|
await this.ensureBrowser();
|
|
300
416
|
if (!this.page || this.page.isClosed()) {
|
|
301
|
-
this.
|
|
417
|
+
if (this.context && typeof this.context.newPage === 'function') {
|
|
418
|
+
this.page = await this.context.newPage();
|
|
419
|
+
} else {
|
|
420
|
+
this.page = await this.browser.newPage();
|
|
421
|
+
}
|
|
302
422
|
await this._applyStealthToPage(this.page);
|
|
303
423
|
}
|
|
304
424
|
return this.page;
|
|
@@ -355,7 +475,7 @@ class BrowserController {
|
|
|
355
475
|
|
|
356
476
|
try {
|
|
357
477
|
const response = await page.goto(url, {
|
|
358
|
-
waitUntil:
|
|
478
|
+
waitUntil: normalizeWaitUntil(options.waitUntil),
|
|
359
479
|
timeout: 30000
|
|
360
480
|
});
|
|
361
481
|
|
|
@@ -622,15 +742,20 @@ class BrowserController {
|
|
|
622
742
|
}
|
|
623
743
|
|
|
624
744
|
async launch(options = {}) {
|
|
745
|
+
void options;
|
|
625
746
|
await this.ensureBrowser();
|
|
626
747
|
return { success: true };
|
|
627
748
|
}
|
|
628
749
|
|
|
629
750
|
isLaunched() {
|
|
630
|
-
|
|
751
|
+
if (this.context) return true;
|
|
752
|
+
return !!(this.browser && typeof this.browser.isConnected === 'function' && this.browser.isConnected());
|
|
631
753
|
}
|
|
632
754
|
|
|
633
755
|
getPageCount() {
|
|
756
|
+
if (this.context && typeof this.context.pages === 'function') {
|
|
757
|
+
try { return this.context.pages().length; } catch { return 0; }
|
|
758
|
+
}
|
|
634
759
|
if (!this.browser) return 0;
|
|
635
760
|
try { return this.browser.pages ? 1 : 0; } catch { return 0; }
|
|
636
761
|
}
|
|
@@ -659,12 +784,49 @@ class BrowserController {
|
|
|
659
784
|
if (this.page && !this.page.isClosed()) {
|
|
660
785
|
await this.page.close().catch(() => { });
|
|
661
786
|
}
|
|
787
|
+
if (this.context) {
|
|
788
|
+
await this.context.close().catch(() => { });
|
|
789
|
+
this.context = null;
|
|
790
|
+
this.browser = null;
|
|
791
|
+
this.page = null;
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
662
794
|
if (this.browser) {
|
|
663
795
|
await this.browser.close().catch(() => { });
|
|
664
796
|
this.browser = null;
|
|
665
797
|
this.page = null;
|
|
666
798
|
}
|
|
667
799
|
}
|
|
800
|
+
|
|
801
|
+
async ensureVirtualDisplay() {
|
|
802
|
+
if (process.platform !== 'linux') {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
if (this.displayProcess && !this.displayProcess.killed) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (this.displayValue && String(this.displayValue).trim()) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const display = ':99';
|
|
813
|
+
const child = spawn('Xvfb', [display, '-screen', '0', '1440x900x24', '-ac', '-nolisten', 'tcp'], {
|
|
814
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
let launchError = '';
|
|
818
|
+
child.stderr.on('data', (chunk) => {
|
|
819
|
+
launchError += chunk.toString();
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
await sleep(1000);
|
|
823
|
+
if (child.exitCode != null) {
|
|
824
|
+
throw new Error(`Failed to start Xvfb: ${String(launchError || `exit code ${child.exitCode}`).trim()}`);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
this.displayProcess = child;
|
|
828
|
+
this.displayValue = display;
|
|
829
|
+
}
|
|
668
830
|
}
|
|
669
831
|
|
|
670
|
-
module.exports = { BrowserController, resolveBrowserExecutablePath, buildIsolatedEvaluationExpression };
|
|
832
|
+
module.exports = { BrowserController, resolveBrowserExecutablePath, buildIsolatedEvaluationExpression, normalizeWaitUntil };
|
|
@@ -31,7 +31,7 @@ class RuntimeHttpClient {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
async waitForHealth(options = {}) {
|
|
34
|
-
const timeoutMs = Number(options.timeoutMs ||
|
|
34
|
+
const timeoutMs = Number(options.timeoutMs || 600000); // Increased from 120s to 10m for bootstrap
|
|
35
35
|
const intervalMs = Number(options.intervalMs || 1000);
|
|
36
36
|
const checkLiveness = options.checkLiveness || (() => true);
|
|
37
37
|
const startedAt = Date.now();
|
|
@@ -65,37 +65,53 @@ class RuntimeHttpClient {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
async request(method, pathname, body, options = {}) {
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const response = await fetch(`${this.baseUrl}${pathname}`, {
|
|
72
|
-
method,
|
|
73
|
-
headers: {
|
|
74
|
-
'content-type': 'application/json',
|
|
75
|
-
...(this.token ? { authorization: `Bearer ${this.token}` } : {}),
|
|
76
|
-
},
|
|
77
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
78
|
-
signal: controller?.signal,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const contentType = response.headers.get('content-type') || '';
|
|
82
|
-
const payload = contentType.includes('application/json')
|
|
83
|
-
? await response.json().catch(() => ({}))
|
|
84
|
-
: { text: await response.text().catch(() => '') };
|
|
68
|
+
const retryCount = Math.max(0, Number(options.retryCount ?? 6));
|
|
69
|
+
const retryDelayMs = Math.max(100, Number(options.retryDelayMs ?? 1000));
|
|
70
|
+
let lastError = null;
|
|
85
71
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
72
|
+
for (let attempt = 0; attempt <= retryCount; attempt += 1) {
|
|
73
|
+
const controller = options.timeoutMs ? new AbortController() : null;
|
|
74
|
+
const timer = controller ? setTimeout(() => controller.abort(new Error(`Request timed out after ${options.timeoutMs} ms.`)), options.timeoutMs) : null;
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(`${this.baseUrl}${pathname}`, {
|
|
77
|
+
method,
|
|
78
|
+
headers: {
|
|
79
|
+
'content-type': 'application/json',
|
|
80
|
+
...(this.token ? { authorization: `Bearer ${this.token}` } : {}),
|
|
81
|
+
},
|
|
82
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
83
|
+
signal: controller?.signal,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const contentType = response.headers.get('content-type') || '';
|
|
87
|
+
const payload = contentType.includes('application/json')
|
|
88
|
+
? await response.json().catch(() => ({}))
|
|
89
|
+
: { text: await response.text().catch(() => '') };
|
|
90
|
+
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const errorMessage = payload?.error || payload?.text || `Runtime request failed: ${response.status}`;
|
|
93
|
+
throw new Error(errorMessage);
|
|
94
|
+
}
|
|
95
|
+
if (typeof this.onActivity === 'function') {
|
|
96
|
+
this.onActivity();
|
|
97
|
+
}
|
|
98
|
+
return payload;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
lastError = error;
|
|
101
|
+
const message = String(error?.message || error);
|
|
102
|
+
const retryable = /fetch failed|ECONNREFUSED|ECONNRESET|socket hang up|timed out/i.test(message);
|
|
103
|
+
if (!retryable || attempt === retryCount) {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
107
|
+
} finally {
|
|
108
|
+
if (timer) {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
}
|
|
97
111
|
}
|
|
98
112
|
}
|
|
113
|
+
|
|
114
|
+
throw lastError || new Error('Runtime request failed.');
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
async requestStream(method, pathname, stream, options = {}) {
|
|
@@ -366,7 +382,7 @@ class LocalVmExecutionBackend {
|
|
|
366
382
|
}
|
|
367
383
|
const session = await this.vmManager.ensureVm(userId);
|
|
368
384
|
this.#touch(userId);
|
|
369
|
-
const client = new RuntimeHttpClient(session.baseUrl, this.token, {
|
|
385
|
+
const client = new RuntimeHttpClient(session.baseUrl, session.guestToken || this.token, {
|
|
370
386
|
onActivity: () => this.#touch(userId),
|
|
371
387
|
});
|
|
372
388
|
try {
|