@testdriverai/runner 7.8.0-test.54 → 7.8.0-test.56

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/lib/automation.js CHANGED
@@ -662,33 +662,68 @@ class Automation extends EventEmitter {
662
662
 
663
663
  async _captureScreenshot() {
664
664
  const sharp = require('sharp');
665
- const tmpFile = path.join(os.tmpdir(), `td_screenshot_${Date.now()}.png`);
665
+ const maxAttempts = 3;
666
666
 
667
- try {
668
- // Capture screenshot via pyautogui → saves to temp file
669
- // Python handles Retina downscale: if physical size differs from logical,
670
- // the image is resized to logical dimensions before saving.
671
- await runPyAutoGUI(
672
- 'img = pyautogui.screenshot()\n' +
673
- 'logical = pyautogui.size()\n' +
674
- 'if img.size[0] != logical[0] or img.size[1] != logical[1]:\n' +
675
- ' from PIL import Image\n' +
676
- ' img = img.resize((logical[0], logical[1]), Image.LANCZOS)\n' +
677
- 'img.save(sys.argv[1], format="PNG")',
678
- [tmpFile],
679
- 20000
680
- );
667
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
668
+ const tmpFile = path.join(os.tmpdir(), `td_screenshot_${Date.now()}.png`);
681
669
 
682
- // Read the PNG and re-encode with sharp (lossless, no compression)
683
- const pngBuffer = fs.readFileSync(tmpFile);
684
- const buffer = await sharp(pngBuffer)
685
- .png({ compressionLevel: 0 })
686
- .toBuffer();
670
+ try {
671
+ // Capture screenshot via pyautogui → saves to temp file
672
+ // Python handles Retina downscale: if physical size differs from logical,
673
+ // the image is resized to logical dimensions before saving.
674
+ await runPyAutoGUI(
675
+ 'img = pyautogui.screenshot()\n' +
676
+ 'logical = pyautogui.size()\n' +
677
+ 'if img.size[0] != logical[0] or img.size[1] != logical[1]:\n' +
678
+ ' from PIL import Image\n' +
679
+ ' img = img.resize((logical[0], logical[1]), Image.LANCZOS)\n' +
680
+ 'img.save(sys.argv[1], format="PNG")',
681
+ [tmpFile],
682
+ 20000
683
+ );
687
684
 
688
- return buffer.toString('base64');
689
- } finally {
690
- // Clean up temp file
691
- try { fs.unlinkSync(tmpFile); } catch {}
685
+ // Read the PNG and re-encode with sharp (lossless, no compression)
686
+ const pngBuffer = fs.readFileSync(tmpFile);
687
+ const image = sharp(pngBuffer);
688
+
689
+ // Detect all-black screenshots (Xvfb/compositor issue)
690
+ if (IS_LINUX) {
691
+ const { channels } = await image.stats();
692
+ // channels[0..2] = R, G, B — check if max pixel value across all channels is near-zero
693
+ const maxPixel = Math.max(
694
+ channels[0]?.max ?? 0,
695
+ channels[1]?.max ?? 0,
696
+ channels[2]?.max ?? 0
697
+ );
698
+ if (maxPixel <= 1) {
699
+ console.warn(`[automation] Screenshot attempt ${attempt}/${maxAttempts}: image is all black (max pixel=${maxPixel})`);
700
+ if (attempt < maxAttempts) {
701
+ // Try to heal: poke the display to trigger a redraw
702
+ try {
703
+ await runPyAutoGUI(
704
+ "import subprocess; " +
705
+ "subprocess.run(['xdotool', 'key', '--clearmodifiers', 'super'], timeout=5); " +
706
+ "subprocess.run(['xset', 's', 'off'], timeout=5); " +
707
+ "subprocess.run(['xset', 's', 'noblank'], timeout=5); " +
708
+ "subprocess.run(['xset', '-dpms'], timeout=5)",
709
+ [],
710
+ 10000
711
+ );
712
+ } catch {}
713
+ // Wait for display to recover
714
+ await new Promise(r => setTimeout(r, 2000));
715
+ continue;
716
+ }
717
+ console.error('[automation] All screenshot attempts returned black — display may be broken');
718
+ }
719
+ }
720
+
721
+ const buffer = await image.png({ compressionLevel: 0 }).toBuffer();
722
+ return buffer.toString('base64');
723
+ } finally {
724
+ // Clean up temp file
725
+ try { fs.unlinkSync(tmpFile); } catch {}
726
+ }
692
727
  }
693
728
  }
694
729
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testdriverai/runner",
3
- "version": "7.8.0-test.54",
3
+ "version": "7.8.0-test.56",
4
4
  "description": "TestDriver Runner - Ably-based remote automation agent with Node.js automation",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -175,7 +175,130 @@ sleep 1
175
175
 
176
176
  echo "[start-desktop] Desktop environment ready"
177
177
 
178
+ # ─── Helper: restart Xvfb ────────────────────────────────────────────────────
179
+ restart_xvfb() {
180
+ echo "[watchdog] Restarting Xvfb..."
181
+ killall Xvfb 2>/dev/null || true
182
+ sleep 1
183
+ rm -f /tmp/.X0-lock /tmp/.X11-unix/X0 2>/dev/null
184
+ Xvfb :0 -ac -screen 0 "${SCREEN_WIDTH}x${SCREEN_HEIGHT}x24" -retro -nolisten tcp &
185
+ XVFB_PID=$!
186
+ # Wait for Xvfb to be ready
187
+ for _w in $(seq 1 10); do
188
+ xdpyinfo -display :0 > /dev/null 2>&1 && break
189
+ sleep 1
190
+ done
191
+ if ! kill -0 $XVFB_PID 2>/dev/null; then
192
+ echo "[watchdog] ERROR: Xvfb failed to restart"
193
+ return 1
194
+ fi
195
+ # Re-disable screen blanking & DPMS on the fresh Xvfb
196
+ xset s off 2>/dev/null || true
197
+ xset s noblank 2>/dev/null || true
198
+ xset -dpms 2>/dev/null || true
199
+ echo "[watchdog] Xvfb restarted (PID: $XVFB_PID)"
200
+ }
201
+
202
+ # ─── Helper: restart xfce4 (mirrors E2B's defunct-process check) ─────────────
203
+ restart_xfce4() {
204
+ echo "[watchdog] Restarting xfce4-session..."
205
+ killall xfce4-session 2>/dev/null || true
206
+ sleep 1
207
+ startxfce4 &
208
+ XFCE4_PID=$!
209
+ sleep 3
210
+ killall xfce4-power-manager 2>/dev/null || true
211
+ killall xfce4-screensaver 2>/dev/null || true
212
+ xfconf-query -c xfwm4 -p /general/use_compositing -s false 2>/dev/null || true
213
+ xset s off 2>/dev/null || true
214
+ xset s noblank 2>/dev/null || true
215
+ xset -dpms 2>/dev/null || true
216
+ echo "[watchdog] xfce4-session restarted (PID: $XFCE4_PID)"
217
+ }
218
+
219
+ # ─── Helper: restart x11vnc ──────────────────────────────────────────────────
220
+ restart_x11vnc() {
221
+ echo "[watchdog] Restarting x11vnc..."
222
+ killall x11vnc 2>/dev/null || true
223
+ sleep 1
224
+ x11vnc -display :0 -forever -nopw -shared -rfbport 5900 \
225
+ -noxdamage -fixscreen V=2 \
226
+ -bg -o /dev/null 2>/dev/null || true
227
+ echo "[watchdog] x11vnc restarted"
228
+ }
229
+
230
+ # ─── Watchdog loop ────────────────────────────────────────────────────────────
231
+ # Monitors Xvfb, xfce4-session, and x11vnc health every 10 seconds.
232
+ # Restarts any component that has crashed or become defunct.
233
+ # Also periodically re-disables screen blanking/compositor as belt-and-suspenders.
234
+ WATCHDOG_INTERVAL=10
235
+ BLANKING_RESET_COUNTER=0
236
+
237
+ watchdog_loop() {
238
+ while true; do
239
+ sleep "$WATCHDOG_INTERVAL"
240
+
241
+ # ── Check Xvfb ──
242
+ if ! pgrep -x Xvfb > /dev/null 2>&1; then
243
+ echo "[watchdog] Xvfb not running! Recovering..."
244
+ restart_xvfb
245
+ # x11vnc and xfce need a running Xvfb, restart them too
246
+ restart_xfce4
247
+ restart_x11vnc
248
+ continue
249
+ fi
250
+
251
+ # Verify Xvfb is actually responding (not just a zombie process)
252
+ if ! xdpyinfo -display :0 > /dev/null 2>&1; then
253
+ echo "[watchdog] Xvfb process exists but display :0 is unresponsive! Recovering..."
254
+ restart_xvfb
255
+ restart_xfce4
256
+ restart_x11vnc
257
+ continue
258
+ fi
259
+
260
+ # ── Check xfce4-session (E2B pattern: detect <defunct> zombie) ──
261
+ XFCE_PID=$(pgrep -x xfce4-session | head -1)
262
+ if [ -z "$XFCE_PID" ]; then
263
+ echo "[watchdog] xfce4-session not running! Restarting..."
264
+ restart_xfce4
265
+ elif ps aux | grep "$XFCE_PID" | grep -v grep | head -1 | grep -q '<defunct>'; then
266
+ echo "[watchdog] xfce4-session is defunct (zombie)! Restarting..."
267
+ restart_xfce4
268
+ fi
269
+
270
+ # ── Check x11vnc ──
271
+ if ! pgrep -x x11vnc > /dev/null 2>&1; then
272
+ echo "[watchdog] x11vnc not running! Restarting..."
273
+ restart_x11vnc
274
+ fi
275
+
276
+ # ── Periodically re-disable screen blanking & compositor (every ~60s) ──
277
+ BLANKING_RESET_COUNTER=$((BLANKING_RESET_COUNTER + 1))
278
+ if [ "$BLANKING_RESET_COUNTER" -ge 6 ]; then
279
+ BLANKING_RESET_COUNTER=0
280
+ xset s off 2>/dev/null || true
281
+ xset s noblank 2>/dev/null || true
282
+ xset -dpms 2>/dev/null || true
283
+ xfconf-query -c xfwm4 -p /general/use_compositing -s false 2>/dev/null || true
284
+ fi
285
+
286
+ # ── Monitor /dev/shm usage ──
287
+ if [ -d /dev/shm ]; then
288
+ SHM_USAGE=$(df /dev/shm 2>/dev/null | awk 'NR==2 {print $5}' | tr -d '%')
289
+ if [ -n "$SHM_USAGE" ] && [ "$SHM_USAGE" -gt 90 ] 2>/dev/null; then
290
+ echo "[watchdog] WARNING: /dev/shm is ${SHM_USAGE}% full — X11 may fail to allocate pixmaps"
291
+ fi
292
+ fi
293
+ done
294
+ }
295
+
296
+ # Start watchdog in background
297
+ watchdog_loop &
298
+ WATCHDOG_PID=$!
299
+ echo "[start-desktop] Watchdog started (PID: $WATCHDOG_PID)"
300
+
178
301
  # Keep the script running so E2B doesn't consider the sandbox stopped
179
302
  # Trap signals for clean shutdown
180
- trap "kill $XVFB_PID $NOVNC_PID 2>/dev/null; exit 0" SIGTERM SIGINT
303
+ trap "kill $XVFB_PID $NOVNC_PID $WATCHDOG_PID 2>/dev/null; exit 0" SIGTERM SIGINT
181
304
  wait