@testdriverai/runner 7.8.0-canary.15 → 7.8.0-test.40
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/README.md +1 -187
- package/lib/ably-service.js +13 -66
- package/lib/automation.js +5 -10
- package/package.json +1 -1
- package/scripts-desktop/start-agent.sh +0 -105
package/README.md
CHANGED
|
@@ -1,187 +1 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
The TestDriver Runner is a desktop automation agent that connects to the TestDriver API via [Ably](https://ably.com/) realtime messaging. It receives commands from the SDK (click, type, find, screenshot, etc.) and executes them on a desktop environment using PyAutoGUI and Sharp.
|
|
4
|
-
|
|
5
|
-
## Architecture
|
|
6
|
-
|
|
7
|
-
The runner operates in two modes:
|
|
8
|
-
|
|
9
|
-
| Mode | Binary | Use Case |
|
|
10
|
-
|------|--------|----------|
|
|
11
|
-
| **Presence Runner** | `testdriver-runner` | Self-registers with the API, enters Ably presence, and waits for SDK sessions to claim it. Used for persistent/pooled runners. |
|
|
12
|
-
| **Sandbox Agent** | `testdriver-sandbox-agent` | Reads pre-provisioned credentials from a config file or environment variables. Used for ephemeral cloud sandboxes (E2B, AWS EC2). |
|
|
13
|
-
|
|
14
|
-
## Prerequisites
|
|
15
|
-
|
|
16
|
-
### System Requirements
|
|
17
|
-
|
|
18
|
-
- **Node.js** >= 18
|
|
19
|
-
- **Python 3** with `pyautogui` and `Pillow`
|
|
20
|
-
- A desktop environment (physical display, VNC, or virtual framebuffer)
|
|
21
|
-
|
|
22
|
-
### Desktop Environment (Linux)
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
# Virtual display + desktop
|
|
26
|
-
apt-get install -y xvfb xfce4 xfce4-terminal dbus-x11 wmctrl
|
|
27
|
-
|
|
28
|
-
# VNC access (optional, for debugging)
|
|
29
|
-
apt-get install -y tigervnc-standalone-server novnc websockify
|
|
30
|
-
|
|
31
|
-
# Python automation
|
|
32
|
-
pip3 install pyautogui python-xlib Pillow
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### Desktop Environment (Windows)
|
|
36
|
-
|
|
37
|
-
- Standard Windows desktop (RDP or console session)
|
|
38
|
-
- Python 3 with `pyautogui` and `Pillow`:
|
|
39
|
-
```powershell
|
|
40
|
-
pip install pyautogui Pillow
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### Chrome
|
|
44
|
-
|
|
45
|
-
Google Chrome or Chrome for Testing must be installed and accessible on `PATH`.
|
|
46
|
-
|
|
47
|
-
## Installation
|
|
48
|
-
|
|
49
|
-
### From the TestDriver API (recommended)
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
curl -fSL -H "x-api-key: $TD_API_KEY" \
|
|
53
|
-
https://api.testdriver.ai/api/v7/runner/download \
|
|
54
|
-
-o /tmp/testdriverai-runner.tgz && \
|
|
55
|
-
npm install -g /tmp/testdriverai-runner.tgz && \
|
|
56
|
-
rm /tmp/testdriverai-runner.tgz
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### From source (development)
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
cd runner
|
|
63
|
-
npm install
|
|
64
|
-
npm start
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Quick Start
|
|
68
|
-
|
|
69
|
-
### Presence Runner
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
export TD_API_KEY="your-team-api-key"
|
|
73
|
-
testdriver-runner
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
The runner will:
|
|
77
|
-
1. Register with the API at `/api/v7/runner/register`
|
|
78
|
-
2. Receive an Ably token and channel
|
|
79
|
-
3. Enter presence on the runner channel
|
|
80
|
-
4. Wait for SDK sessions to claim it
|
|
81
|
-
|
|
82
|
-
### Sandbox Agent
|
|
83
|
-
|
|
84
|
-
The sandbox agent reads credentials from a JSON config file that the API provisions (via SSM, cloud-init, etc.):
|
|
85
|
-
|
|
86
|
-
**Linux:** `/tmp/testdriver-agent.json`
|
|
87
|
-
**Windows:** `C:\Windows\Temp\testdriver-agent.json`
|
|
88
|
-
|
|
89
|
-
```json
|
|
90
|
-
{
|
|
91
|
-
"sandboxId": "sb-abc123",
|
|
92
|
-
"ably": {
|
|
93
|
-
"token": "ably-token-string",
|
|
94
|
-
"channel": "testdriver:env:team:sandbox"
|
|
95
|
-
},
|
|
96
|
-
"apiRoot": "https://api.testdriver.ai",
|
|
97
|
-
"apiKey": "team-api-key"
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
Start the agent (it will wait up to 5 minutes for the config file to appear):
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
testdriver-sandbox-agent
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Or pass credentials via environment variables instead:
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
export SANDBOX_ID="my-sandbox"
|
|
111
|
-
export ABLY_TOKEN='{"token":"..."}'
|
|
112
|
-
export ABLY_CHANNEL="testdriver:env:team:sandbox"
|
|
113
|
-
testdriver-sandbox-agent
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## Environment Variables
|
|
117
|
-
|
|
118
|
-
### Required
|
|
119
|
-
|
|
120
|
-
| Variable | Description |
|
|
121
|
-
|----------|-------------|
|
|
122
|
-
| `TD_API_KEY` | Team API key (presence runner mode) |
|
|
123
|
-
|
|
124
|
-
### Optional
|
|
125
|
-
|
|
126
|
-
| Variable | Default | Description |
|
|
127
|
-
|----------|---------|-------------|
|
|
128
|
-
| `TD_API_ROOT` | Per `TD_ENV` | API server URL |
|
|
129
|
-
| `TD_ENV` | `stable` | Environment (`dev` / `test` / `canary` / `stable`) |
|
|
130
|
-
| `TD_RUNNER_ID` | Auto-generated UUID | Fixed runner identifier |
|
|
131
|
-
| `TD_RUNNER_SINGLE` | `false` | Exit after one session |
|
|
132
|
-
| `TD_RUNNER_OS` | Auto-detected | OS capability advertised to API |
|
|
133
|
-
| `TD_VNC_URL` | Auto-detected | Public VNC URL override |
|
|
134
|
-
| `TD_NOVNC_PORT` | Auto-detected | noVNC WebSocket proxy port |
|
|
135
|
-
| `SANDBOX_ID` | Auto-generated | Sandbox identifier (agent mode) |
|
|
136
|
-
| `ABLY_TOKEN` | From config file | Ably auth token JSON (agent mode) |
|
|
137
|
-
| `ABLY_CHANNEL` | From config file | Ably channel name (agent mode) |
|
|
138
|
-
| `CONFIG_PATH` | `/tmp/testdriver-agent.json` | Config file path override (agent mode) |
|
|
139
|
-
| `SCREEN_WIDTH` | `1366` | Virtual display width (Linux) |
|
|
140
|
-
| `SCREEN_HEIGHT` | `768` | Virtual display height (Linux) |
|
|
141
|
-
| `DISPLAY` | `:0` | X11 display (Linux) |
|
|
142
|
-
|
|
143
|
-
## Logs
|
|
144
|
-
|
|
145
|
-
| Platform | Runner Log | Agent Log |
|
|
146
|
-
|----------|-----------|-----------|
|
|
147
|
-
| Linux/macOS | `/tmp/testdriver-runner.log` | `/tmp/testdriver-agent.log` |
|
|
148
|
-
| Windows | `C:\Windows\Temp\testdriver-runner.log` | `C:\Windows\Temp\testdriver-agent.log` |
|
|
149
|
-
|
|
150
|
-
## Desktop Scripts
|
|
151
|
-
|
|
152
|
-
Helper scripts in `scripts-desktop/` for managing the Linux desktop environment:
|
|
153
|
-
|
|
154
|
-
| Script | Purpose |
|
|
155
|
-
|--------|---------|
|
|
156
|
-
| `start-desktop.sh` | Starts Xvfb, XFCE, D-Bus, disables screen blanking |
|
|
157
|
-
| `launch_chrome.sh` | Launches Chrome with standard flags |
|
|
158
|
-
| `launch_chrome_for_testing.sh` | Launches Chrome for Testing with remote debugging (port 9222) |
|
|
159
|
-
| `control_window.sh` | Window management (minimize, restore, focus) via wmctrl |
|
|
160
|
-
|
|
161
|
-
## Deployment
|
|
162
|
-
|
|
163
|
-
### AWS AMI (Packer)
|
|
164
|
-
|
|
165
|
-
See `packer/` for Packer templates that build AMIs with the runner pre-installed. The AMI includes the full desktop stack, Chrome, Python, and the runner.
|
|
166
|
-
|
|
167
|
-
### E2B Sandboxes
|
|
168
|
-
|
|
169
|
-
The E2B template installs the runner in a Dockerfile. See `sdk/setup/e2b/` for the recommended setup.
|
|
170
|
-
|
|
171
|
-
### Docker
|
|
172
|
-
|
|
173
|
-
```bash
|
|
174
|
-
TD_API_KEY=your-key docker compose up --build
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
## Updating
|
|
178
|
-
|
|
179
|
-
Re-download and reinstall:
|
|
180
|
-
|
|
181
|
-
```bash
|
|
182
|
-
curl -fSL -H "x-api-key: $TD_API_KEY" \
|
|
183
|
-
https://api.testdriver.ai/api/v7/runner/download \
|
|
184
|
-
-o /tmp/testdriverai-runner.tgz && \
|
|
185
|
-
npm install -g /tmp/testdriverai-runner.tgz && \
|
|
186
|
-
rm /tmp/testdriverai-runner.tgz
|
|
187
|
-
```
|
|
1
|
+
# runner
|
package/lib/ably-service.js
CHANGED
|
@@ -266,7 +266,7 @@ class AblyService extends EventEmitter {
|
|
|
266
266
|
});
|
|
267
267
|
|
|
268
268
|
// Subscribe to commands — save subscription ref for historyBeforeSubscribe()
|
|
269
|
-
this.
|
|
269
|
+
this._commandSubscription = await this._sessionChannel.subscribe('command', async (msg) => {
|
|
270
270
|
const message = msg.data;
|
|
271
271
|
if (!message) return;
|
|
272
272
|
|
|
@@ -275,9 +275,6 @@ class AblyService extends EventEmitter {
|
|
|
275
275
|
|
|
276
276
|
this.emit('log', `Command received: ${type} (requestId=${requestId})`);
|
|
277
277
|
|
|
278
|
-
// Stop re-publishing runner.ready once we get the first command
|
|
279
|
-
this._stopReadySignal();
|
|
280
|
-
|
|
281
278
|
// Per-command timeout: use message.timeout if provided, else default 120s
|
|
282
279
|
// Prevents hanging forever if screenshot capture or S3 upload stalls
|
|
283
280
|
const commandTimeout = (message.timeout && message.timeout > 0)
|
|
@@ -331,8 +328,7 @@ class AblyService extends EventEmitter {
|
|
|
331
328
|
} else {
|
|
332
329
|
await executeCommand();
|
|
333
330
|
}
|
|
334
|
-
};
|
|
335
|
-
this._commandSubscription = await this._sessionChannel.subscribe('command', this._onCommandMsg);
|
|
331
|
+
});
|
|
336
332
|
|
|
337
333
|
// ─── Ably connection state monitoring → Sentry ─────────────────────────
|
|
338
334
|
this._ably.connection.on((stateChange) => {
|
|
@@ -418,8 +414,8 @@ class AblyService extends EventEmitter {
|
|
|
418
414
|
|
|
419
415
|
// Detect discontinuity: channel re-attached but message continuity was lost.
|
|
420
416
|
// Use historyBeforeSubscribe() on each subscription to recover missed messages.
|
|
421
|
-
if (current === 'attached' && stateChange.resumed === false && previous
|
|
422
|
-
this.emit('log', `Ably channel [session]: DISCONTINUITY (resumed=false)${reasonMsg ? ' — ' + reasonMsg : ''}`);
|
|
417
|
+
if (current === 'attached' && stateChange.resumed === false && previous) {
|
|
418
|
+
this.emit('log', `Ably channel [session]: DISCONTINUITY (resumed=false)${reasonMsg ? ' — ' + reasonMsg : ''}`);
|
|
423
419
|
|
|
424
420
|
Sentry.withScope((scope) => {
|
|
425
421
|
scope.setTag('ably.client', 'runner');
|
|
@@ -435,7 +431,7 @@ class AblyService extends EventEmitter {
|
|
|
435
431
|
}
|
|
436
432
|
|
|
437
433
|
// Subscribe to control messages — save subscription ref for historyBeforeSubscribe()
|
|
438
|
-
this.
|
|
434
|
+
this._controlSubscription = await this._sessionChannel.subscribe('control', async (msg) => {
|
|
439
435
|
const message = msg.data;
|
|
440
436
|
if (!message) return;
|
|
441
437
|
|
|
@@ -454,15 +450,14 @@ class AblyService extends EventEmitter {
|
|
|
454
450
|
this._debugMode = !!message.enabled;
|
|
455
451
|
this.emit('log', `Debug mode ${this._debugMode ? 'enabled' : 'disabled'}`);
|
|
456
452
|
}
|
|
457
|
-
};
|
|
458
|
-
this._controlSubscription = await this._sessionChannel.subscribe('control', this._onControlMsg);
|
|
453
|
+
});
|
|
459
454
|
|
|
460
455
|
this.emit('log', 'Listening for commands on Ably');
|
|
461
456
|
|
|
462
457
|
// Signal readiness to SDK — commands sent before this would be lost
|
|
463
458
|
const readyPayload = {
|
|
464
459
|
type: 'runner.ready',
|
|
465
|
-
os:
|
|
460
|
+
os: 'windows',
|
|
466
461
|
sandboxId: this._sandboxId,
|
|
467
462
|
runnerVersion: getLocalVersion() || 'unknown',
|
|
468
463
|
timestamp: Date.now(),
|
|
@@ -476,39 +471,6 @@ class AblyService extends EventEmitter {
|
|
|
476
471
|
}
|
|
477
472
|
await this._sessionChannel.publish('control', readyPayload);
|
|
478
473
|
this.emit('log', 'Published runner.ready signal');
|
|
479
|
-
|
|
480
|
-
// Re-publish runner.ready every 3s for up to 60s.
|
|
481
|
-
// The SDK may connect after the first publish (race condition),
|
|
482
|
-
// and Ably channel history may not be enabled. Repeating ensures
|
|
483
|
-
// the SDK catches at least one live runner.ready message.
|
|
484
|
-
this._readyInterval = setInterval(async () => {
|
|
485
|
-
try {
|
|
486
|
-
readyPayload.timestamp = Date.now();
|
|
487
|
-
await this._sessionChannel.publish('control', readyPayload);
|
|
488
|
-
this.emit('log', 'Re-published runner.ready signal');
|
|
489
|
-
} catch (err) {
|
|
490
|
-
this.emit('log', `Failed to re-publish runner.ready: ${err.message}`);
|
|
491
|
-
}
|
|
492
|
-
}, 3000);
|
|
493
|
-
|
|
494
|
-
// Stop after 60s regardless
|
|
495
|
-
this._readyTimeout = setTimeout(() => {
|
|
496
|
-
this._stopReadySignal();
|
|
497
|
-
}, 60000);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Stop the repeated runner.ready signal (called on first command or after timeout).
|
|
502
|
-
*/
|
|
503
|
-
_stopReadySignal() {
|
|
504
|
-
if (this._readyInterval) {
|
|
505
|
-
clearInterval(this._readyInterval);
|
|
506
|
-
this._readyInterval = null;
|
|
507
|
-
}
|
|
508
|
-
if (this._readyTimeout) {
|
|
509
|
-
clearTimeout(this._readyTimeout);
|
|
510
|
-
this._readyTimeout = null;
|
|
511
|
-
}
|
|
512
474
|
}
|
|
513
475
|
|
|
514
476
|
/**
|
|
@@ -557,37 +519,24 @@ class AblyService extends EventEmitter {
|
|
|
557
519
|
/**
|
|
558
520
|
* Recover missed messages after a channel discontinuity.
|
|
559
521
|
* Uses historyBeforeSubscribe() on each subscription, which guarantees
|
|
560
|
-
* no gap between historical and live messages.
|
|
561
|
-
* is dispatched through the same handler that processes live messages
|
|
562
|
-
* so that missed commands are actually executed.
|
|
522
|
+
* no gap between historical and live messages.
|
|
563
523
|
*/
|
|
564
524
|
async _recoverFromDiscontinuity() {
|
|
565
525
|
const subs = [
|
|
566
|
-
{ name: 'command', sub: this._commandSubscription
|
|
567
|
-
{ name: 'control', sub: this._controlSubscription
|
|
526
|
+
{ name: 'command', sub: this._commandSubscription },
|
|
527
|
+
{ name: 'control', sub: this._controlSubscription },
|
|
568
528
|
];
|
|
569
|
-
for (const { name, sub
|
|
529
|
+
for (const { name, sub } of subs) {
|
|
570
530
|
if (!sub) continue;
|
|
571
531
|
try {
|
|
572
532
|
this.emit('log', `Discontinuity recovery: fetching historyBeforeSubscribe for ${name}...`);
|
|
573
533
|
let page = await sub.historyBeforeSubscribe({ limit: 100 });
|
|
574
534
|
let recovered = 0;
|
|
575
535
|
while (page) {
|
|
576
|
-
|
|
577
|
-
recovered++;
|
|
578
|
-
try {
|
|
579
|
-
if (handler) {
|
|
580
|
-
this.emit('log', `Replaying recovered ${name} message (requestId=${item.data && item.data.requestId || 'none'})`);
|
|
581
|
-
await handler(item);
|
|
582
|
-
}
|
|
583
|
-
} catch (replayErr) {
|
|
584
|
-
this.emit('log', `Error replaying recovered ${name} message: ${replayErr.message}`);
|
|
585
|
-
Sentry.captureException(replayErr);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
536
|
+
recovered += page.items.length;
|
|
588
537
|
page = page.hasNext() ? await page.next() : null;
|
|
589
538
|
}
|
|
590
|
-
this.emit('log', `Discontinuity recovery:
|
|
539
|
+
this.emit('log', `Discontinuity recovery: found ${recovered} ${name} message(s) in gap`);
|
|
591
540
|
} catch (err) {
|
|
592
541
|
this.emit('log', `Discontinuity recovery failed for ${name}: ${err.message}`);
|
|
593
542
|
Sentry.captureException(err);
|
|
@@ -653,8 +602,6 @@ class AblyService extends EventEmitter {
|
|
|
653
602
|
async close() {
|
|
654
603
|
this.emit('log', 'Closing Ably service...');
|
|
655
604
|
|
|
656
|
-
this._stopReadySignal();
|
|
657
|
-
|
|
658
605
|
if (this._statsInterval) {
|
|
659
606
|
clearInterval(this._statsInterval);
|
|
660
607
|
this._statsInterval = null;
|
package/lib/automation.js
CHANGED
|
@@ -45,10 +45,8 @@ const API_KEY = process.env.TD_API_KEY;
|
|
|
45
45
|
// shell injection and escaping issues.
|
|
46
46
|
|
|
47
47
|
const PYTHON = IS_WINDOWS ? 'python' : 'python3';
|
|
48
|
-
// On Linux, ensure DISPLAY is set (use env var or fallback to :0)
|
|
49
|
-
// The os.environ.get() preserves the parent's DISPLAY setting for E2B's :1 display
|
|
50
48
|
const PY_IMPORT = IS_LINUX
|
|
51
|
-
? "import os; os.environ
|
|
49
|
+
? "import os; os.environ['DISPLAY'] = ':0'; import pyautogui, sys; pyautogui.FAILSAFE = False; "
|
|
52
50
|
: 'import pyautogui, sys; pyautogui.FAILSAFE = False; ';
|
|
53
51
|
|
|
54
52
|
/**
|
|
@@ -527,14 +525,11 @@ class Automation extends EventEmitter {
|
|
|
527
525
|
const timeout = Math.ceil((data.timeout || 300000) / 1000); // ms to seconds
|
|
528
526
|
const requestId = data.requestId;
|
|
529
527
|
|
|
530
|
-
// Buffer stdout chunks to ~
|
|
528
|
+
// Buffer stdout chunks to ~16KB before emitting over Ably.
|
|
531
529
|
// This reduces message count while keeping each message well under
|
|
532
|
-
// Ably's 64KB limit.
|
|
533
|
-
//
|
|
534
|
-
|
|
535
|
-
// commands. The SDK accumulates these chunks and reconstructs the
|
|
536
|
-
// full stdout — the final response only carries returncode + stderr.
|
|
537
|
-
const CHUNK_FLUSH_SIZE = 32 * 1024; // 32KB
|
|
530
|
+
// Ably's 64KB limit. The SDK accumulates these chunks and reconstructs
|
|
531
|
+
// the full stdout — the final response only carries returncode + stderr.
|
|
532
|
+
const CHUNK_FLUSH_SIZE = 16 * 1024; // 16KB
|
|
538
533
|
let chunkBuffer = '';
|
|
539
534
|
const flushChunkBuffer = () => {
|
|
540
535
|
if (chunkBuffer.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# ─── TestDriver Sandbox Agent Startup ────────────────────────────────────────
|
|
3
|
-
# Starts the sandbox-agent.js (Ably-based automation agent) inside the E2B
|
|
4
|
-
# sandbox. This script is called by the API after writing the config file
|
|
5
|
-
# to /tmp/testdriver-agent.json.
|
|
6
|
-
#
|
|
7
|
-
# This matches the Windows runner pattern: the agent runs locally on the
|
|
8
|
-
# sandbox and executes commands via pyautogui (instead of @e2b/desktop RPC).
|
|
9
|
-
#
|
|
10
|
-
# Usage: bash /opt/testdriver-runner/scripts-desktop/start-agent.sh [&]
|
|
11
|
-
#
|
|
12
|
-
# Prerequisites:
|
|
13
|
-
# - Desktop environment running (start-desktop.sh completed)
|
|
14
|
-
# - Config file at /tmp/testdriver-agent.json with Ably credentials
|
|
15
|
-
# - Node.js installed
|
|
16
|
-
# - Runner installed at /opt/testdriver-runner
|
|
17
|
-
|
|
18
|
-
set -e
|
|
19
|
-
|
|
20
|
-
export DISPLAY="${DISPLAY:-:0}"
|
|
21
|
-
export XAUTHORITY="${XAUTHORITY:-${HOME}/.Xauthority}"
|
|
22
|
-
|
|
23
|
-
RUNNER_DIR="/opt/testdriver-runner"
|
|
24
|
-
CONFIG_PATH="/tmp/testdriver-agent.json"
|
|
25
|
-
LOG_FILE="/tmp/sandbox-agent.log"
|
|
26
|
-
PID_FILE="/tmp/sandbox-agent.pid"
|
|
27
|
-
|
|
28
|
-
log() {
|
|
29
|
-
echo "[$(date -Iseconds)] [start-agent] $1" | tee -a "$LOG_FILE"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
# ─── Check if already running ─────────────────────────────────────────────────
|
|
33
|
-
if [ -f "$PID_FILE" ]; then
|
|
34
|
-
existing_pid=$(cat "$PID_FILE")
|
|
35
|
-
if kill -0 "$existing_pid" 2>/dev/null; then
|
|
36
|
-
log "Agent already running (PID: $existing_pid), exiting"
|
|
37
|
-
exit 0
|
|
38
|
-
else
|
|
39
|
-
log "Stale PID file found, removing"
|
|
40
|
-
rm -f "$PID_FILE"
|
|
41
|
-
fi
|
|
42
|
-
fi
|
|
43
|
-
|
|
44
|
-
# ─── Verify prerequisites ─────────────────────────────────────────────────────
|
|
45
|
-
if [ ! -d "$RUNNER_DIR" ]; then
|
|
46
|
-
log "ERROR: Runner not found at $RUNNER_DIR"
|
|
47
|
-
exit 1
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
if [ ! -f "$RUNNER_DIR/sandbox-agent.js" ]; then
|
|
51
|
-
log "ERROR: sandbox-agent.js not found in $RUNNER_DIR"
|
|
52
|
-
exit 1
|
|
53
|
-
fi
|
|
54
|
-
|
|
55
|
-
if ! command -v node &> /dev/null; then
|
|
56
|
-
log "ERROR: Node.js not installed"
|
|
57
|
-
exit 1
|
|
58
|
-
fi
|
|
59
|
-
|
|
60
|
-
# ─── Wait for config file (with timeout) ─────────────────────────────────────
|
|
61
|
-
# The API writes the config file before calling this script, but we add a
|
|
62
|
-
# brief wait just in case there's any race condition.
|
|
63
|
-
WAIT_TIMEOUT=30
|
|
64
|
-
WAIT_INTERVAL=1
|
|
65
|
-
elapsed=0
|
|
66
|
-
|
|
67
|
-
log "Waiting for config file: $CONFIG_PATH"
|
|
68
|
-
while [ ! -f "$CONFIG_PATH" ] && [ $elapsed -lt $WAIT_TIMEOUT ]; do
|
|
69
|
-
sleep $WAIT_INTERVAL
|
|
70
|
-
elapsed=$((elapsed + WAIT_INTERVAL))
|
|
71
|
-
done
|
|
72
|
-
|
|
73
|
-
if [ ! -f "$CONFIG_PATH" ]; then
|
|
74
|
-
log "ERROR: Config file not found after ${WAIT_TIMEOUT}s: $CONFIG_PATH"
|
|
75
|
-
exit 1
|
|
76
|
-
fi
|
|
77
|
-
|
|
78
|
-
log "Config file found"
|
|
79
|
-
|
|
80
|
-
# ─── Start the agent ──────────────────────────────────────────────────────────
|
|
81
|
-
log "Starting sandbox-agent.js..."
|
|
82
|
-
log "DISPLAY=$DISPLAY, RUNNER_DIR=$RUNNER_DIR"
|
|
83
|
-
|
|
84
|
-
# Run in background, redirect output to log file
|
|
85
|
-
cd "$RUNNER_DIR"
|
|
86
|
-
nohup node sandbox-agent.js >> "$LOG_FILE" 2>&1 &
|
|
87
|
-
AGENT_PID=$!
|
|
88
|
-
|
|
89
|
-
# Write PID file for process management
|
|
90
|
-
echo "$AGENT_PID" > "$PID_FILE"
|
|
91
|
-
|
|
92
|
-
log "Agent started (PID: $AGENT_PID)"
|
|
93
|
-
log "Log file: $LOG_FILE"
|
|
94
|
-
|
|
95
|
-
# Brief pause to catch any immediate startup errors
|
|
96
|
-
sleep 2
|
|
97
|
-
|
|
98
|
-
if kill -0 "$AGENT_PID" 2>/dev/null; then
|
|
99
|
-
log "Agent running successfully"
|
|
100
|
-
exit 0
|
|
101
|
-
else
|
|
102
|
-
log "ERROR: Agent exited unexpectedly. Check $LOG_FILE for details"
|
|
103
|
-
tail -20 "$LOG_FILE" | while read line; do log " $line"; done
|
|
104
|
-
exit 1
|
|
105
|
-
fi
|