Merge pull request #1273 from pikasTech/fix/2255-observe-screenshot-timeout
fix: bound web observe screenshot commands
This commit is contained in:
@@ -21,6 +21,7 @@ const jobId = safeId(process.env.UNIDESK_WEB_OBSERVE_JOB_ID || "webobs-" + Date.
|
||||
const targetPath = process.env.UNIDESK_WEB_OBSERVE_TARGET_PATH || "/workbench";
|
||||
const sampleIntervalMs = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_SAMPLE_INTERVAL_MS, 5000);
|
||||
const screenshotIntervalMs = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_SCREENSHOT_INTERVAL_MS, 300000);
|
||||
const screenshotCaptureTimeoutMs = boundedInteger(process.env.UNIDESK_WEB_OBSERVE_SCREENSHOT_CAPTURE_TIMEOUT_MS, 15000, 1000, 120000);
|
||||
const maxSamples = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_SAMPLES, 0);
|
||||
const observerRefreshIntervalMs = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_OBSERVER_REFRESH_INTERVAL_MS, 180000);
|
||||
const viewport = parseViewport(process.env.UNIDESK_WEB_OBSERVE_VIEWPORT || "1440x900");
|
||||
@@ -68,6 +69,7 @@ let activeCommandId = null;
|
||||
let stopping = false;
|
||||
let terminalStatus = "starting";
|
||||
let lastScreenshotAtMs = 0;
|
||||
let screenshotCaptureState = null;
|
||||
let auth = null;
|
||||
let pageLoadSeq = 0;
|
||||
let controlPageEpoch = 0;
|
||||
@@ -3539,7 +3541,7 @@ async function samplePage(reason, options = {}) {
|
||||
await sampleOnePage(observerPage, { reason, groupSeq, pageRole: "observer", targetPageId: observerPageId, pageEpoch: observerPageEpoch }).catch((error) => appendJsonl(files.errors, eventRecord("observer-sample-error", { pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, error: errorSummary(error) })));
|
||||
}
|
||||
if (options?.screenshot !== false && screenshotIntervalMs > 0 && Date.now() - lastScreenshotAtMs >= screenshotIntervalMs) {
|
||||
await withHardTimeout(captureScreenshot("checkpoint", "jpeg"), 15000, "captureScreenshot checkpoint exceeded 15s")
|
||||
await captureScreenshot("checkpoint", "jpeg")
|
||||
.catch((error) => appendJsonl(files.errors, eventRecord("screenshot-error", { pageRole: "control", pageId, error: errorSummary(error) })));
|
||||
}
|
||||
await writeHeartbeat({ status: terminalStatus });
|
||||
@@ -4267,18 +4269,68 @@ function digestSessionRail(value) {
|
||||
|
||||
async function captureScreenshot(reason, imageType = "png") {
|
||||
if (!page || page.isClosed()) throw new Error("page is not available for screenshot");
|
||||
if (screenshotCaptureState && screenshotCaptureState.settled !== true) {
|
||||
const ageMs = Date.now() - Number(screenshotCaptureState.startedAtMs || Date.now());
|
||||
const error = new Error("screenshot capture already in progress");
|
||||
error.details = {
|
||||
reason,
|
||||
currentUrl: currentPageUrl(),
|
||||
pageId,
|
||||
activeReason: screenshotCaptureState.reason,
|
||||
activeStartedAt: screenshotCaptureState.startedAt,
|
||||
activeAgeMs: ageMs,
|
||||
activeTimedOut: screenshotCaptureState.timedOut === true,
|
||||
timeoutMs: screenshotCaptureTimeoutMs,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
lastScreenshotAtMs = Date.now();
|
||||
throw error;
|
||||
}
|
||||
artifactSeq += 1;
|
||||
const safeReason = safeId(String(reason || "manual")).slice(0, 40) || "manual";
|
||||
const type = imageType === "jpeg" || imageType === "jpg" ? "jpeg" : "png";
|
||||
const ext = type === "jpeg" ? "jpg" : "png";
|
||||
const file = path.join(dirs.screenshots, String(sampleSeq).padStart(6, "0") + "_" + String(artifactSeq).padStart(4, "0") + "_" + safeReason + "." + ext);
|
||||
const options = type === "jpeg" ? { path: file, type: "jpeg", quality: 70, fullPage: false } : { path: file, type: "png", fullPage: false };
|
||||
await page.screenshot(options);
|
||||
const meta = await fileMeta(file);
|
||||
const artifact = { seq: artifactSeq, sampleSeq, ts: new Date().toISOString(), kind: "screenshot", reason, path: file, type, byteCount: meta.byteCount, sha256: meta.sha256, pageId, currentUrl: currentPageUrl() };
|
||||
await appendJsonl(files.artifacts, artifact);
|
||||
lastScreenshotAtMs = Date.now();
|
||||
return artifact;
|
||||
const timeoutMs = screenshotCaptureTimeoutMs;
|
||||
const options = type === "jpeg"
|
||||
? { path: file, type: "jpeg", quality: 70, fullPage: false, animations: "disabled", timeout: timeoutMs }
|
||||
: { path: file, type: "png", fullPage: false, animations: "disabled", timeout: timeoutMs };
|
||||
const state = { reason, startedAtMs: Date.now(), startedAt: new Date().toISOString(), timeoutMs, settled: false, timedOut: false };
|
||||
screenshotCaptureState = state;
|
||||
const screenshotPromise = page.screenshot(options)
|
||||
.then((value) => {
|
||||
state.settled = true;
|
||||
return value;
|
||||
})
|
||||
.catch((error) => {
|
||||
state.settled = true;
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
if (screenshotCaptureState === state) screenshotCaptureState = null;
|
||||
});
|
||||
try {
|
||||
await withHardTimeout(screenshotPromise, timeoutMs + 1000, "captureScreenshot " + safeReason + " exceeded " + timeoutMs + "ms");
|
||||
const meta = await fileMeta(file);
|
||||
const artifact = { seq: artifactSeq, sampleSeq, ts: new Date().toISOString(), kind: "screenshot", reason, path: file, type, byteCount: meta.byteCount, sha256: meta.sha256, pageId, currentUrl: currentPageUrl(), timeoutMs };
|
||||
await appendJsonl(files.artifacts, artifact);
|
||||
lastScreenshotAtMs = Date.now();
|
||||
return artifact;
|
||||
} catch (error) {
|
||||
if (String(error?.message || "").includes("exceeded " + timeoutMs + "ms")) state.timedOut = true;
|
||||
lastScreenshotAtMs = Date.now();
|
||||
const wrapped = error instanceof Error ? error : new Error(String(error));
|
||||
wrapped.details = {
|
||||
...(wrapped.details || {}),
|
||||
reason,
|
||||
currentUrl: currentPageUrl(),
|
||||
pageId,
|
||||
timeoutMs,
|
||||
file,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
throw wrapped;
|
||||
}
|
||||
}
|
||||
|
||||
async function captureCommandScreenshot(command) {
|
||||
|
||||
Reference in New Issue
Block a user