@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 +59 -24
- package/package.json +1 -1
- package/scripts-desktop/start-desktop.sh +124 -1
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
|
|
665
|
+
const maxAttempts = 3;
|
|
666
666
|
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
@@ -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
|