neoagent 2.3.1-beta.85 → 2.3.1-beta.86
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/server/guest-agent.android.package.json +13 -0
- package/server/guest-agent.browser.package.json +14 -0
- package/server/guest_agent.js +61 -44
- 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/routes/android.js +2 -11
- package/server/routes/browser.js +2 -2
- package/server/services/ai/capabilityHealth.js +6 -14
- package/server/services/android/android_bootstrap_worker.js +2 -2
- package/server/services/android/controller.js +528 -132
- package/server/services/browser/controller.js +51 -68
- package/server/services/runtime/backends/local-vm.js +16 -3
- package/server/services/runtime/guest_bootstrap.js +224 -56
- package/server/services/runtime/manager.js +53 -15
- package/server/services/runtime/qemu.js +149 -24
- package/server/services/runtime/settings.js +9 -14
- package/server/services/runtime/validation.js +10 -11
- package/server/utils/deployment.js +4 -4
- package/server/guest-agent.package.json +0 -15
|
@@ -26,6 +26,7 @@ const VIEWPORTS = [
|
|
|
26
26
|
|
|
27
27
|
function resolveBrowserExecutablePath() {
|
|
28
28
|
const explicitPath =
|
|
29
|
+
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ||
|
|
29
30
|
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
|
30
31
|
process.env.CHROME_BIN ||
|
|
31
32
|
process.env.CHROMIUM_BIN;
|
|
@@ -34,7 +35,6 @@ function resolveBrowserExecutablePath() {
|
|
|
34
35
|
|
|
35
36
|
const bundledCandidates = [
|
|
36
37
|
() => require('playwright-chromium').chromium.executablePath(),
|
|
37
|
-
() => require('playwright').chromium.executablePath(),
|
|
38
38
|
];
|
|
39
39
|
for (const resolveBundled of bundledCandidates) {
|
|
40
40
|
try {
|
|
@@ -76,22 +76,11 @@ function resolveBrowserExecutablePath() {
|
|
|
76
76
|
return platformCandidates.find((candidate) => fs.existsSync(candidate)) || null;
|
|
77
77
|
}
|
|
78
78
|
|
|
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
79
|
function installPlaywrightBrowserBinary(browserName) {
|
|
89
|
-
const packageRoot = path.dirname(require.resolve('playwright/package.json'));
|
|
80
|
+
const packageRoot = path.dirname(require.resolve('playwright-chromium/package.json'));
|
|
90
81
|
const cliPath = path.join(packageRoot, 'cli.js');
|
|
91
82
|
return new Promise((resolve, reject) => {
|
|
92
|
-
const args =
|
|
93
|
-
? [cliPath, 'install', '--no-shell', 'chromium']
|
|
94
|
-
: [cliPath, 'install', browserName];
|
|
83
|
+
const args = [cliPath, 'install', '--no-shell', browserName];
|
|
95
84
|
const child = spawn(process.execPath, args, {
|
|
96
85
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
97
86
|
});
|
|
@@ -161,13 +150,29 @@ function normalizeWaitUntil(waitUntil) {
|
|
|
161
150
|
return 'domcontentloaded';
|
|
162
151
|
}
|
|
163
152
|
|
|
153
|
+
function clearChromiumSingletonLocks(profileDir) {
|
|
154
|
+
const lockEntries = [
|
|
155
|
+
'SingletonLock',
|
|
156
|
+
'SingletonSocket',
|
|
157
|
+
'SingletonCookie',
|
|
158
|
+
'SingletonStartupLock',
|
|
159
|
+
'DevToolsActivePort',
|
|
160
|
+
];
|
|
161
|
+
for (const entry of lockEntries) {
|
|
162
|
+
const targetPath = path.join(profileDir, entry);
|
|
163
|
+
try {
|
|
164
|
+
fs.rmSync(targetPath, { force: true, recursive: true });
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
164
169
|
class BrowserController {
|
|
165
170
|
constructor(options = {}) {
|
|
166
171
|
this.io = options.io || null;
|
|
167
172
|
this.userId = options.userId != null ? String(options.userId) : null;
|
|
168
173
|
this.artifactStore = options.artifactStore || null;
|
|
169
174
|
this.runtimeBackend = options.runtimeBackend || 'host';
|
|
170
|
-
this.engine =
|
|
175
|
+
this.engine = 'chromium';
|
|
171
176
|
this.browser = null;
|
|
172
177
|
this.context = null;
|
|
173
178
|
this.page = null;
|
|
@@ -327,9 +332,7 @@ class BrowserController {
|
|
|
327
332
|
this._userAgent = USER_AGENTS[rand(0, USER_AGENTS.length - 1)];
|
|
328
333
|
this._viewport = VIEWPORTS[rand(0, VIEWPORTS.length - 1)];
|
|
329
334
|
|
|
330
|
-
let executablePath =
|
|
331
|
-
? resolveFirefoxExecutablePath()
|
|
332
|
-
: resolveBrowserExecutablePath();
|
|
335
|
+
let executablePath = resolveBrowserExecutablePath();
|
|
333
336
|
if (!executablePath) {
|
|
334
337
|
if (!this.browserBinaryInstallPromise) {
|
|
335
338
|
this.browserBinaryInstallPromise = installPlaywrightBrowserBinary(this.engine);
|
|
@@ -339,9 +342,7 @@ class BrowserController {
|
|
|
339
342
|
} finally {
|
|
340
343
|
this.browserBinaryInstallPromise = null;
|
|
341
344
|
}
|
|
342
|
-
executablePath =
|
|
343
|
-
? resolveFirefoxExecutablePath()
|
|
344
|
-
: resolveBrowserExecutablePath();
|
|
345
|
+
executablePath = resolveBrowserExecutablePath();
|
|
345
346
|
}
|
|
346
347
|
|
|
347
348
|
if (!executablePath) {
|
|
@@ -353,53 +354,35 @@ class BrowserController {
|
|
|
353
354
|
...(this.displayValue ? { DISPLAY: this.displayValue } : {}),
|
|
354
355
|
};
|
|
355
356
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
}
|
|
357
|
+
const launchArgs = [
|
|
358
|
+
'--no-sandbox',
|
|
359
|
+
'--disable-setuid-sandbox',
|
|
360
|
+
'--disable-dev-shm-usage',
|
|
361
|
+
'--disable-crash-reporter',
|
|
362
|
+
'--disable-background-networking',
|
|
363
|
+
'--disable-component-update',
|
|
364
|
+
'--disable-blink-features=AutomationControlled',
|
|
365
|
+
'--disable-infobars',
|
|
366
|
+
'--no-first-run',
|
|
367
|
+
'--no-default-browser-check',
|
|
368
|
+
'--disable-gpu',
|
|
369
|
+
'--lang=en-US,en',
|
|
370
|
+
`--window-size=${this._viewport.width},${this._viewport.height}`,
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
const playwright = require('playwright-chromium');
|
|
374
|
+
clearChromiumSingletonLocks(this.profileDir);
|
|
375
|
+
this.context = await playwright.chromium.launchPersistentContext(this.profileDir, {
|
|
376
|
+
headless: false,
|
|
377
|
+
executablePath,
|
|
378
|
+
env: launchEnv,
|
|
379
|
+
args: launchArgs,
|
|
380
|
+
viewport: this._viewport,
|
|
381
|
+
ignoreHTTPSErrors: false,
|
|
382
|
+
timeout: 120000,
|
|
383
|
+
});
|
|
384
|
+
this.browser = typeof this.context.browser === 'function' ? this.context.browser() : null;
|
|
385
|
+
this.page = this.context.pages()[0] || await this.context.newPage();
|
|
403
386
|
await this._applyStealthToPage(this.page);
|
|
404
387
|
})();
|
|
405
388
|
|
|
@@ -23,6 +23,18 @@ function assertPathInside(baseDir, candidatePath, label) {
|
|
|
23
23
|
return resolvedCandidate;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function isPidAlive(pid) {
|
|
27
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
process.kill(pid, 0);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
class RuntimeHttpClient {
|
|
27
39
|
constructor(baseUrl, token = '', options = {}) {
|
|
28
40
|
this.baseUrl = String(baseUrl || '').replace(/\/+$/, '');
|
|
@@ -341,6 +353,7 @@ class VmAndroidProvider {
|
|
|
341
353
|
class LocalVmExecutionBackend {
|
|
342
354
|
constructor(options = {}) {
|
|
343
355
|
this.vmManager = options.vmManager;
|
|
356
|
+
this.runtimeProfile = options.runtimeProfile === 'android' ? 'android' : 'browser_cli';
|
|
344
357
|
this.token = options.token || process.env.NEOAGENT_VM_GUEST_TOKEN || '';
|
|
345
358
|
this.artifactStore = options.artifactStore || null;
|
|
346
359
|
this.lastActivity = new Map();
|
|
@@ -364,12 +377,12 @@ class LocalVmExecutionBackend {
|
|
|
364
377
|
const now = Date.now();
|
|
365
378
|
for (const [userId, lastUsed] of this.lastActivity.entries()) {
|
|
366
379
|
if (now - lastUsed > IDLE_TIMEOUT_MS) {
|
|
367
|
-
console.log(`[Runtime] User ${userId} runtime idle for ${Math.round((now - lastUsed) / 1000)}s, shutting down VM.`);
|
|
380
|
+
console.log(`[Runtime:${this.runtimeProfile}] User ${userId} runtime idle for ${Math.round((now - lastUsed) / 1000)}s, shutting down VM.`);
|
|
368
381
|
this.lastActivity.delete(userId);
|
|
369
382
|
try {
|
|
370
383
|
await this.vmManager?.killVm?.(userId);
|
|
371
384
|
} catch (err) {
|
|
372
|
-
console.error(`[Runtime] Failed to shut down idle VM for user ${userId}:`, err.message);
|
|
385
|
+
console.error(`[Runtime:${this.runtimeProfile}] Failed to shut down idle VM for user ${userId}:`, err.message);
|
|
373
386
|
}
|
|
374
387
|
}
|
|
375
388
|
}
|
|
@@ -391,7 +404,7 @@ class LocalVmExecutionBackend {
|
|
|
391
404
|
checkLiveness: () => {
|
|
392
405
|
const key = String(userId || '').trim();
|
|
393
406
|
const session = this.vmManager.instances.get(key);
|
|
394
|
-
return session && session.process &&
|
|
407
|
+
return Boolean(session && session.process && isPidAlive(session.process.pid));
|
|
395
408
|
},
|
|
396
409
|
});
|
|
397
410
|
} catch (error) {
|
|
@@ -6,15 +6,24 @@ const { DATA_DIR } = require('../../../runtime/paths');
|
|
|
6
6
|
const VM_ROOT = path.join(DATA_DIR, 'runtime-vms');
|
|
7
7
|
const GUEST_BOOTSTRAP_ROOT = path.join(VM_ROOT, 'guest-bootstrap');
|
|
8
8
|
const REPO_ROOT = path.resolve(__dirname, '../../..');
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
]
|
|
9
|
+
const GUEST_PAYLOAD_PROFILES = Object.freeze({
|
|
10
|
+
browser_cli: [
|
|
11
|
+
{ source: 'server/guest-agent.browser.package.json', target: 'package.json' },
|
|
12
|
+
{ source: 'runtime/env.js', target: 'runtime/env.js' },
|
|
13
|
+
{ source: 'runtime/paths.js', target: 'runtime/paths.js' },
|
|
14
|
+
{ source: 'server/guest_agent.js', target: 'server/guest_agent.js' },
|
|
15
|
+
{ source: 'server/services/cli', target: 'server/services/cli' },
|
|
16
|
+
{ source: 'server/services/browser', target: 'server/services/browser' },
|
|
17
|
+
],
|
|
18
|
+
android: [
|
|
19
|
+
{ source: 'server/guest-agent.android.package.json', target: 'package.json' },
|
|
20
|
+
{ source: 'runtime/env.js', target: 'runtime/env.js' },
|
|
21
|
+
{ source: 'runtime/paths.js', target: 'runtime/paths.js' },
|
|
22
|
+
{ source: 'server/guest_agent.js', target: 'server/guest_agent.js' },
|
|
23
|
+
{ source: 'server/services/cli', target: 'server/services/cli' },
|
|
24
|
+
{ source: 'server/services/android', target: 'server/services/android' },
|
|
25
|
+
],
|
|
26
|
+
});
|
|
18
27
|
|
|
19
28
|
fs.mkdirSync(GUEST_BOOTSTRAP_ROOT, { recursive: true });
|
|
20
29
|
|
|
@@ -22,15 +31,20 @@ function encodeGuestToken(value) {
|
|
|
22
31
|
return Buffer.from(String(value || ''), 'utf8').toString('base64');
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
function
|
|
34
|
+
function normalizeRuntimeProfile(runtimeProfile) {
|
|
35
|
+
return runtimeProfile === 'android' ? 'android' : 'browser_cli';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createGuestPayloadArchive(seedDir, runtimeProfile = 'browser_cli') {
|
|
26
39
|
const seedRoot = path.dirname(seedDir);
|
|
27
40
|
const stagingRoot = path.join(seedRoot, 'guest-payload');
|
|
28
41
|
const archivePath = path.join(seedRoot, 'guest-payload.tar.gz');
|
|
42
|
+
const payloadEntries = GUEST_PAYLOAD_PROFILES[normalizeRuntimeProfile(runtimeProfile)];
|
|
29
43
|
fs.rmSync(stagingRoot, { recursive: true, force: true });
|
|
30
44
|
fs.rmSync(archivePath, { force: true });
|
|
31
45
|
fs.mkdirSync(stagingRoot, { recursive: true });
|
|
32
46
|
|
|
33
|
-
for (const entry of
|
|
47
|
+
for (const entry of payloadEntries) {
|
|
34
48
|
const sourcePath = path.join(REPO_ROOT, entry.source);
|
|
35
49
|
const targetPath = path.join(stagingRoot, entry.target);
|
|
36
50
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
@@ -59,10 +73,17 @@ function createCloudInitScript({
|
|
|
59
73
|
guestToken,
|
|
60
74
|
guestPayloadPath = '/var/lib/neoagent/guest-payload.tar.gz',
|
|
61
75
|
guestAgentPort = 8421,
|
|
76
|
+
runtimeProfile = 'browser_cli',
|
|
62
77
|
}) {
|
|
78
|
+
const normalizedProfile = normalizeRuntimeProfile(runtimeProfile);
|
|
79
|
+
const includeBrowser = normalizedProfile === 'browser_cli';
|
|
80
|
+
const guestUtilityPackages = includeBrowser
|
|
81
|
+
? 'curl ca-certificates gnupg git rsync unzip xvfb dbus-x11'
|
|
82
|
+
: 'curl ca-certificates gnupg git rsync unzip dbus-x11 adb';
|
|
63
83
|
const guestTokenB64 = encodeGuestToken(guestToken);
|
|
64
84
|
const envFile = '/etc/neoagent/neoagent.env';
|
|
65
85
|
const appDir = '/opt/neoagent';
|
|
86
|
+
const playwrightBrowsersPath = `${appDir}/.playwright-browsers`;
|
|
66
87
|
const bootstrapMarker = '/var/lib/neoagent/bootstrap-complete';
|
|
67
88
|
const browserReadyMarker = '/var/lib/neoagent/browser-runtime-ready';
|
|
68
89
|
const browserDepsMarker = '/var/lib/neoagent/browser-deps-installed';
|
|
@@ -74,6 +95,7 @@ function createCloudInitScript({
|
|
|
74
95
|
'',
|
|
75
96
|
'export DEBIAN_FRONTEND=noninteractive',
|
|
76
97
|
`APP_DIR=${JSON.stringify(appDir)}`,
|
|
98
|
+
`PLAYWRIGHT_BROWSERS_PATH=${JSON.stringify(playwrightBrowsersPath)}`,
|
|
77
99
|
`BOOTSTRAP_MARKER=${JSON.stringify(bootstrapMarker)}`,
|
|
78
100
|
`BROWSER_READY_MARKER=${JSON.stringify(browserReadyMarker)}`,
|
|
79
101
|
`BROWSER_DEPS_MARKER=${JSON.stringify(browserDepsMarker)}`,
|
|
@@ -123,53 +145,47 @@ function createCloudInitScript({
|
|
|
123
145
|
'',
|
|
124
146
|
`printf '%s\n' ${JSON.stringify(`NEOAGENT_VM_GUEST_TOKEN_B64=${guestTokenB64}`)} > "$ENV_FILE"`,
|
|
125
147
|
`printf '%s\n' ${JSON.stringify(`NEOAGENT_GUEST_AGENT_PORT=${guestAgentPort}`)} >> "$ENV_FILE"`,
|
|
148
|
+
`printf '%s\n' ${JSON.stringify(`NEOAGENT_GUEST_PROFILE=${normalizedProfile}`)} >> "$ENV_FILE"`,
|
|
126
149
|
'chmod 0600 "$ENV_FILE"',
|
|
127
150
|
'',
|
|
128
151
|
'cd "$APP_DIR"',
|
|
129
152
|
'export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1',
|
|
153
|
+
'export PLAYWRIGHT_BROWSERS_PATH="$PLAYWRIGHT_BROWSERS_PATH"',
|
|
130
154
|
'if [ ! -d node_modules ] || [ ! -f node_modules/.neoagent-bootstrap-stamp ] || [ package.json -nt node_modules/.neoagent-bootstrap-stamp ]; then',
|
|
131
155
|
' echo "Installing npm dependencies..."',
|
|
132
|
-
' retry_cmd npm
|
|
133
|
-
' retry_cmd npm install --omit=dev --no-audit --no-fund || { echo "Error: npm install failed." >&2; exit 1; }',
|
|
156
|
+
' retry_cmd npm install --omit=dev --ignore-scripts --prefer-offline --no-audit --no-fund || { echo "Error: npm install failed." >&2; exit 1; }',
|
|
134
157
|
' mkdir -p node_modules',
|
|
135
158
|
' date > node_modules/.neoagent-bootstrap-stamp',
|
|
136
159
|
'fi',
|
|
137
160
|
'',
|
|
138
|
-
'if [ ! -f "$BROWSER_DEPS_MARKER" ]; then',
|
|
139
|
-
' echo "Updating package lists..."',
|
|
140
|
-
' retry_cmd apt-get update || echo "Warning: apt-get update failed, proceeding with cached lists."',
|
|
141
|
-
'',
|
|
142
|
-
' echo "Installing browser runtime dependencies..."',
|
|
143
|
-
' retry_cmd apt-get install -y --no-install-recommends \\',
|
|
144
|
-
' curl ca-certificates gnupg git rsync unzip \\',
|
|
145
|
-
' dbus-x11 \\',
|
|
146
|
-
' xvfb \\',
|
|
147
|
-
' libatk1.0-0 libatk-bridge2.0-0 libatspi2.0-0 libcups2 \\',
|
|
148
|
-
' libx11-xcb1 libgtk-3-0 libnss3 libnspr4 libxcomposite1 libxdamage1 \\',
|
|
149
|
-
' libxrandr2 libxkbcommon0 libasound2t64 libgbm1 libdrm2 libdbus-1-3 \\',
|
|
150
|
-
' libpango-1.0-0 libpangocairo-1.0-0 libxshmfence1 || echo "Warning: Some browser dependencies failed to install."',
|
|
151
|
-
' touch "$BROWSER_DEPS_MARKER"',
|
|
152
|
-
'else',
|
|
153
|
-
' echo "Browser runtime dependencies already installed; skipping apt install."',
|
|
154
|
-
'fi',
|
|
155
161
|
'',
|
|
156
|
-
'
|
|
157
|
-
'
|
|
158
|
-
|
|
159
|
-
' systemctl start neoagent-guest-agent.service || true',
|
|
160
|
-
'fi',
|
|
161
|
-
'echo "NeoAgent guest agent is available; continuing browser runtime provisioning..."',
|
|
162
|
+
'echo "Ensuring guest runtime utilities..."',
|
|
163
|
+
'retry_cmd apt-get update || echo "Warning: apt-get update failed, proceeding with cached lists."',
|
|
164
|
+
`retry_cmd apt-get install -y --no-install-recommends ${guestUtilityPackages} || { echo "Error: Failed to install required guest runtime utilities." >&2; exit 1; }`,
|
|
162
165
|
'',
|
|
163
|
-
'
|
|
164
|
-
'PLAYWRIGHT_STAMP="$PLAYWRIGHT_BROWSERS_PATH/.firefox-installed"',
|
|
165
|
-
'mkdir -p "$PLAYWRIGHT_BROWSERS_PATH"',
|
|
166
|
-
'if [ ! -f "$PLAYWRIGHT_STAMP" ] || [ package.json -nt "$PLAYWRIGHT_STAMP" ]; then',
|
|
167
|
-
' echo "Installing Playwright browsers..."',
|
|
168
|
-
' PLAYWRIGHT_BROWSERS_PATH="$PLAYWRIGHT_BROWSERS_PATH" retry_cmd npx playwright install firefox || { echo "Error: Playwright browser install failed." >&2; exit 1; }',
|
|
169
|
-
' date > "$PLAYWRIGHT_STAMP"',
|
|
170
|
-
'fi',
|
|
166
|
+
'echo "NeoAgent guest runtime payload is ready."',
|
|
171
167
|
'',
|
|
172
|
-
|
|
168
|
+
...(includeBrowser
|
|
169
|
+
? [
|
|
170
|
+
'echo "Continuing browser runtime provisioning..."',
|
|
171
|
+
'PLAYWRIGHT_BROWSERS_PATH="$APP_DIR/.playwright-browsers"',
|
|
172
|
+
'PLAYWRIGHT_STAMP="$PLAYWRIGHT_BROWSERS_PATH/.chromium-installed"',
|
|
173
|
+
'mkdir -p "$PLAYWRIGHT_BROWSERS_PATH"',
|
|
174
|
+
'if [ ! -f "$BROWSER_DEPS_MARKER" ]; then',
|
|
175
|
+
' echo "Installing Playwright browser dependencies..."',
|
|
176
|
+
' PLAYWRIGHT_BROWSERS_PATH="$PLAYWRIGHT_BROWSERS_PATH" retry_cmd npx playwright install-deps chromium || { echo "Error: Playwright dependency install failed." >&2; exit 1; }',
|
|
177
|
+
' touch "$BROWSER_DEPS_MARKER"',
|
|
178
|
+
'fi',
|
|
179
|
+
'if [ ! -f "$PLAYWRIGHT_STAMP" ] || [ package.json -nt "$PLAYWRIGHT_STAMP" ]; then',
|
|
180
|
+
' echo "Installing Playwright browsers..."',
|
|
181
|
+
' PLAYWRIGHT_BROWSERS_PATH="$PLAYWRIGHT_BROWSERS_PATH" retry_cmd npx playwright install chromium || { echo "Error: Playwright browser install failed." >&2; exit 1; }',
|
|
182
|
+
' date > "$PLAYWRIGHT_STAMP"',
|
|
183
|
+
'fi',
|
|
184
|
+
'touch "$BROWSER_READY_MARKER"',
|
|
185
|
+
]
|
|
186
|
+
: [
|
|
187
|
+
'rm -f "$BROWSER_READY_MARKER"',
|
|
188
|
+
]),
|
|
173
189
|
'touch "$BOOTSTRAP_MARKER"',
|
|
174
190
|
'echo "NeoAgent guest bootstrap completed."',
|
|
175
191
|
'',
|
|
@@ -178,16 +194,109 @@ function createCloudInitScript({
|
|
|
178
194
|
|
|
179
195
|
function createCloudInitUserData({
|
|
180
196
|
guestToken,
|
|
181
|
-
guestPayloadBase64,
|
|
197
|
+
guestPayloadBase64 = '',
|
|
182
198
|
guestAgentPort = 8421,
|
|
199
|
+
runtimeMode = 'template',
|
|
200
|
+
runtimeProfile = 'browser_cli',
|
|
183
201
|
}) {
|
|
202
|
+
const normalizedProfile = normalizeRuntimeProfile(runtimeProfile);
|
|
203
|
+
const includeBrowser = normalizedProfile === 'browser_cli';
|
|
204
|
+
const guestAgentInnerCommand = includeBrowser
|
|
205
|
+
? 'set -a; . /etc/neoagent/neoagent.env; set +a; cd /opt/neoagent && env DISPLAY=:99 PLAYWRIGHT_BROWSERS_PATH=/opt/neoagent/.playwright-browsers /usr/bin/env node server/guest_agent.js 2>&1 | tee -a /var/log/neoagent-guest-agent.log >/dev/console'
|
|
206
|
+
: 'set -a; . /etc/neoagent/neoagent.env; set +a; cd /opt/neoagent && /usr/bin/env node server/guest_agent.js 2>&1 | tee -a /var/log/neoagent-guest-agent.log >/dev/console';
|
|
207
|
+
const guestAgentLaunchCommand = `nohup /bin/sh -lc ${JSON.stringify(guestAgentInnerCommand)} </dev/null >/dev/null 2>&1 &`;
|
|
184
208
|
const guestTokenB64 = encodeGuestToken(guestToken);
|
|
185
209
|
const bootstrapScript = createCloudInitScript({
|
|
186
210
|
guestToken,
|
|
187
211
|
guestPayloadPath: '/var/lib/neoagent/guest-payload.tar.gz',
|
|
188
212
|
guestAgentPort,
|
|
213
|
+
runtimeProfile: normalizedProfile,
|
|
189
214
|
});
|
|
190
215
|
|
|
216
|
+
if (runtimeMode === 'user') {
|
|
217
|
+
return [
|
|
218
|
+
'#cloud-config',
|
|
219
|
+
'package_update: false',
|
|
220
|
+
'write_files:',
|
|
221
|
+
' - path: /etc/neoagent/neoagent.env',
|
|
222
|
+
" permissions: '0600'",
|
|
223
|
+
' owner: root:root',
|
|
224
|
+
' content: |',
|
|
225
|
+
` NEOAGENT_VM_GUEST_TOKEN_B64=${guestTokenB64}`,
|
|
226
|
+
` NEOAGENT_GUEST_AGENT_PORT=${guestAgentPort}`,
|
|
227
|
+
` NEOAGENT_GUEST_PROFILE=${normalizedProfile}`,
|
|
228
|
+
...(includeBrowser
|
|
229
|
+
? [
|
|
230
|
+
' - path: /etc/systemd/system/neoagent-xvfb.service',
|
|
231
|
+
" permissions: '0644'",
|
|
232
|
+
' owner: root:root',
|
|
233
|
+
' content: |',
|
|
234
|
+
' [Unit]',
|
|
235
|
+
' Description=NeoAgent virtual display',
|
|
236
|
+
' After=network-online.target',
|
|
237
|
+
' Wants=network-online.target',
|
|
238
|
+
'',
|
|
239
|
+
' [Service]',
|
|
240
|
+
' Type=simple',
|
|
241
|
+
' ExecStart=/usr/bin/Xvfb :99 -screen 0 1440x900x24 -ac -nolisten tcp',
|
|
242
|
+
' Restart=always',
|
|
243
|
+
' RestartSec=2',
|
|
244
|
+
' StandardOutput=journal+console',
|
|
245
|
+
' StandardError=journal+console',
|
|
246
|
+
'',
|
|
247
|
+
' [Install]',
|
|
248
|
+
' WantedBy=multi-user.target',
|
|
249
|
+
]
|
|
250
|
+
: []),
|
|
251
|
+
' - path: /etc/systemd/system/neoagent-guest-agent.service',
|
|
252
|
+
" permissions: '0644'",
|
|
253
|
+
' owner: root:root',
|
|
254
|
+
' content: |',
|
|
255
|
+
' [Unit]',
|
|
256
|
+
' Description=NeoAgent guest agent',
|
|
257
|
+
' After=network-online.target',
|
|
258
|
+
...(includeBrowser ? [' After=neoagent-xvfb.service'] : []),
|
|
259
|
+
' ConditionPathExists=/etc/neoagent/neoagent.env',
|
|
260
|
+
' Wants=network-online.target',
|
|
261
|
+
'',
|
|
262
|
+
' [Service]',
|
|
263
|
+
' Type=simple',
|
|
264
|
+
' EnvironmentFile=/etc/neoagent/neoagent.env',
|
|
265
|
+
' ExecStartPre=/bin/mkdir -p /var/lib/neoagent',
|
|
266
|
+
...(includeBrowser
|
|
267
|
+
? [
|
|
268
|
+
' ExecStartPre=/usr/bin/touch /var/lib/neoagent/browser-runtime-ready',
|
|
269
|
+
' ExecStartPre=/bin/sh -lc \'for _ in $(seq 1 30); do [ -S /tmp/.X11-unix/X99 ] && exit 0; sleep 1; done; exit 1\'',
|
|
270
|
+
' Environment=DISPLAY=:99',
|
|
271
|
+
' Environment=PLAYWRIGHT_BROWSERS_PATH=/opt/neoagent/.playwright-browsers',
|
|
272
|
+
]
|
|
273
|
+
: [
|
|
274
|
+
' ExecStartPre=/bin/sh -lc \'rm -f /var/lib/neoagent/browser-runtime-ready || true\'',
|
|
275
|
+
]),
|
|
276
|
+
' ExecStartPre=/usr/bin/touch /var/lib/neoagent/bootstrap-complete',
|
|
277
|
+
' WorkingDirectory=/opt/neoagent',
|
|
278
|
+
' ExecStart=/usr/bin/env node /opt/neoagent/server/guest_agent.js',
|
|
279
|
+
' Restart=always',
|
|
280
|
+
' RestartSec=5',
|
|
281
|
+
' StandardOutput=journal+console',
|
|
282
|
+
' StandardError=journal+console',
|
|
283
|
+
'',
|
|
284
|
+
' [Install]',
|
|
285
|
+
' WantedBy=multi-user.target',
|
|
286
|
+
'runcmd:',
|
|
287
|
+
' - [bash, -lc, "systemctl daemon-reload"]',
|
|
288
|
+
...(includeBrowser
|
|
289
|
+
? [
|
|
290
|
+
' - [bash, -lc, "systemctl enable neoagent-xvfb.service"]',
|
|
291
|
+
' - [bash, -lc, "systemctl start neoagent-xvfb.service"]',
|
|
292
|
+
]
|
|
293
|
+
: []),
|
|
294
|
+
' - [bash, -lc, "systemctl enable neoagent-guest-agent.service"]',
|
|
295
|
+
' - [bash, -lc, "systemctl start --no-block neoagent-guest-agent.service"]',
|
|
296
|
+
'',
|
|
297
|
+
].join('\n');
|
|
298
|
+
}
|
|
299
|
+
|
|
191
300
|
return [
|
|
192
301
|
'#cloud-config',
|
|
193
302
|
'package_update: false',
|
|
@@ -195,9 +304,10 @@ function createCloudInitUserData({
|
|
|
195
304
|
' - path: /etc/neoagent/neoagent.env',
|
|
196
305
|
" permissions: '0600'",
|
|
197
306
|
' owner: root:root',
|
|
198
|
-
|
|
307
|
+
' content: |',
|
|
199
308
|
` NEOAGENT_VM_GUEST_TOKEN_B64=${guestTokenB64}`,
|
|
200
309
|
` NEOAGENT_GUEST_AGENT_PORT=${guestAgentPort}`,
|
|
310
|
+
` NEOAGENT_GUEST_PROFILE=${normalizedProfile}`,
|
|
201
311
|
' - path: /var/lib/neoagent/guest-payload.tar.gz',
|
|
202
312
|
" permissions: '0644'",
|
|
203
313
|
' owner: root:root',
|
|
@@ -216,12 +326,22 @@ function createCloudInitUserData({
|
|
|
216
326
|
' [Unit]',
|
|
217
327
|
' Description=NeoAgent guest agent',
|
|
218
328
|
' After=network-online.target',
|
|
329
|
+
' After=cloud-final.service',
|
|
330
|
+
' After=neoagent-guest-bootstrap.service',
|
|
331
|
+
...(includeBrowser ? [' After=neoagent-xvfb.service'] : []),
|
|
332
|
+
' ConditionPathExists=/etc/neoagent/neoagent.env',
|
|
333
|
+
...(includeBrowser ? [' Requires=neoagent-xvfb.service'] : []),
|
|
219
334
|
' Wants=network-online.target',
|
|
220
335
|
'',
|
|
221
336
|
' [Service]',
|
|
222
337
|
' Type=simple',
|
|
223
338
|
' EnvironmentFile=/etc/neoagent/neoagent.env',
|
|
224
|
-
|
|
339
|
+
...(includeBrowser
|
|
340
|
+
? [
|
|
341
|
+
' Environment=DISPLAY=:99',
|
|
342
|
+
' Environment=PLAYWRIGHT_BROWSERS_PATH=/opt/neoagent/.playwright-browsers',
|
|
343
|
+
]
|
|
344
|
+
: []),
|
|
225
345
|
' WorkingDirectory=/opt/neoagent',
|
|
226
346
|
' ExecStart=/usr/bin/env node /opt/neoagent/server/guest_agent.js',
|
|
227
347
|
' Restart=always',
|
|
@@ -231,6 +351,29 @@ function createCloudInitUserData({
|
|
|
231
351
|
'',
|
|
232
352
|
' [Install]',
|
|
233
353
|
' WantedBy=multi-user.target',
|
|
354
|
+
...(includeBrowser
|
|
355
|
+
? [
|
|
356
|
+
' - path: /etc/systemd/system/neoagent-xvfb.service',
|
|
357
|
+
" permissions: '0644'",
|
|
358
|
+
' owner: root:root',
|
|
359
|
+
' content: |',
|
|
360
|
+
' [Unit]',
|
|
361
|
+
' Description=NeoAgent virtual display',
|
|
362
|
+
' After=network-online.target',
|
|
363
|
+
' Wants=network-online.target',
|
|
364
|
+
'',
|
|
365
|
+
' [Service]',
|
|
366
|
+
' Type=simple',
|
|
367
|
+
' ExecStart=/usr/bin/Xvfb :99 -screen 0 1440x900x24 -ac -nolisten tcp',
|
|
368
|
+
' Restart=always',
|
|
369
|
+
' RestartSec=2',
|
|
370
|
+
' StandardOutput=journal+console',
|
|
371
|
+
' StandardError=journal+console',
|
|
372
|
+
'',
|
|
373
|
+
' [Install]',
|
|
374
|
+
' WantedBy=multi-user.target',
|
|
375
|
+
]
|
|
376
|
+
: []),
|
|
234
377
|
' - path: /etc/systemd/system/neoagent-guest-bootstrap.service',
|
|
235
378
|
" permissions: '0644'",
|
|
236
379
|
' owner: root:root',
|
|
@@ -249,8 +392,14 @@ function createCloudInitUserData({
|
|
|
249
392
|
' WantedBy=multi-user.target',
|
|
250
393
|
'runcmd:',
|
|
251
394
|
' - [bash, -lc, "systemctl daemon-reload"]',
|
|
252
|
-
|
|
253
|
-
|
|
395
|
+
...(includeBrowser
|
|
396
|
+
? [
|
|
397
|
+
' - [bash, -lc, "systemctl enable neoagent-xvfb.service"]',
|
|
398
|
+
' - [bash, -lc, "systemctl start neoagent-xvfb.service"]',
|
|
399
|
+
]
|
|
400
|
+
: []),
|
|
401
|
+
' - [bash, -lc, "/usr/local/bin/neoagent-guest-bootstrap.sh"]',
|
|
402
|
+
` - [bash, -lc, ${JSON.stringify(guestAgentLaunchCommand)}]`,
|
|
254
403
|
'',
|
|
255
404
|
].join('\n');
|
|
256
405
|
}
|
|
@@ -263,6 +412,21 @@ function createCloudInitMetaData({ instanceId, localHostName }) {
|
|
|
263
412
|
].join('\n');
|
|
264
413
|
}
|
|
265
414
|
|
|
415
|
+
function resolveCloudInitIdentity(userRoot) {
|
|
416
|
+
const relativePath = path.relative(VM_ROOT, path.resolve(userRoot || ''));
|
|
417
|
+
const normalized = relativePath
|
|
418
|
+
.split(path.sep)
|
|
419
|
+
.filter(Boolean)
|
|
420
|
+
.map((segment) => segment.replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '').slice(0, 32))
|
|
421
|
+
.filter(Boolean)
|
|
422
|
+
.join('-');
|
|
423
|
+
const scope = normalized || 'default';
|
|
424
|
+
return {
|
|
425
|
+
instanceId: `neoagent-${scope}`,
|
|
426
|
+
localHostName: `neoagent-${scope}`,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
266
430
|
function commandExists(command) {
|
|
267
431
|
const probe = spawnSync(
|
|
268
432
|
process.platform === 'win32' ? 'where' : 'bash',
|
|
@@ -413,6 +577,8 @@ function ensureGuestBootstrapSeed({
|
|
|
413
577
|
guestToken,
|
|
414
578
|
guestAgentPort = 8421,
|
|
415
579
|
guestArch = 'x64',
|
|
580
|
+
runtimeMode = 'template',
|
|
581
|
+
runtimeProfile = 'browser_cli',
|
|
416
582
|
}) {
|
|
417
583
|
const seedRoot = path.join(userRoot, 'cloud-init');
|
|
418
584
|
const seedDir = path.join(seedRoot, 'seed');
|
|
@@ -423,17 +589,18 @@ function ensureGuestBootstrapSeed({
|
|
|
423
589
|
const userDataPath = path.join(seedDir, 'user-data');
|
|
424
590
|
const metaDataPath = path.join(seedDir, 'meta-data');
|
|
425
591
|
const startupNshPath = path.join(seedDir, 'startup.nsh');
|
|
426
|
-
const
|
|
427
|
-
|
|
592
|
+
const guestPayloadBase64 = runtimeMode === 'user'
|
|
593
|
+
? ''
|
|
594
|
+
: fs.readFileSync(createGuestPayloadArchive(seedDir, runtimeProfile)).toString('base64');
|
|
428
595
|
const userData = createCloudInitUserData({
|
|
429
596
|
guestToken,
|
|
430
597
|
guestPayloadBase64,
|
|
431
598
|
guestAgentPort,
|
|
599
|
+
runtimeMode,
|
|
600
|
+
runtimeProfile,
|
|
432
601
|
});
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
localHostName: `neoagent-${path.basename(userRoot)}`,
|
|
436
|
-
});
|
|
602
|
+
const identity = resolveCloudInitIdentity(userRoot);
|
|
603
|
+
const metaData = createCloudInitMetaData(identity);
|
|
437
604
|
const startupNsh = guestArch === 'arm64'
|
|
438
605
|
? [
|
|
439
606
|
'@echo -off',
|
|
@@ -480,6 +647,7 @@ function ensureGuestBootstrapSeed({
|
|
|
480
647
|
module.exports = {
|
|
481
648
|
createCloudInitMetaData,
|
|
482
649
|
createCloudInitUserData,
|
|
650
|
+
createCloudInitScript,
|
|
483
651
|
createSeedIso,
|
|
484
652
|
ensureGuestBootstrapSeed,
|
|
485
653
|
GUEST_BOOTSTRAP_ROOT,
|