Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions apps/server/src/preview/ServerBrowser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2616,6 +2616,70 @@ it.live("a full server takes back the slot of the longest idle agent tab nobody
).pipe(Effect.provide(layer)),
);

it.effect(
"a viewer whose screencast Chromium never starts still attaches and lets snapshots run",
() =>
Effect.scoped(
Effect.gen(function* () {
const { browser, broker, tabId } = yield* ready;
const context = contexts[0]!;
const stalledStarts = yield* Queue.unbounded<void>();
const openSession = context.newCDPSession;
context.newCDPSession = async (target: unknown) => {
const session = await openSession(target);
const answer = session.send.getMockImplementation()!;
session.send.mockImplementation(async (method: string, input?: unknown) => {
if (method !== "Page.startScreencast") return answer(method, input);
Queue.offerUnsafe(stalledStarts, undefined);
return new Promise<never>(() => {});
});
return session;
};
const attaching = yield* browser
.attachViewer(viewerInput(tabId, true))
.pipe(Effect.forkScoped);
yield* Queue.take(stalledStarts);
yield* TestClock.adjust("5 seconds");
const viewer = yield* Fiber.join(attaching);
expect((yield* Queue.takeAll(viewer.output)).map((item) => item._tag)).toContain("frame");
const snapshotting = yield* broker
.invoke<PreviewAutomationSnapshot>({ scope, tabId, operation: "snapshot", input: {} })
.pipe(Effect.forkScoped);
yield* Queue.take(stalledStarts);
yield* TestClock.adjust("5 seconds");
expect((yield* Fiber.join(snapshotting)).url).toBeDefined();
}),
).pipe(Effect.provide(layer)),
);

it.effect("viewer cleanup detaches after a stalled stop command", () =>
Effect.scoped(
Effect.gen(function* () {
const { browser, tabId } = yield* ready;
const viewerScope = yield* Scope.make();
yield* browser
.attachViewer(viewerInput(tabId, true))
.pipe(Effect.provideService(Scope.Scope, viewerScope));
const session = contexts[0]!.sessions.at(-1)!;
const stopped = yield* Queue.unbounded<void>();
const stalledStop = Promise.withResolvers<Record<string, unknown>>();
const answer = session.send.getMockImplementation()!;
session.send.mockImplementation(async (method: string, input?: unknown) => {
if (method !== "Page.stopScreencast") return answer(method, input);
Queue.offerUnsafe(stopped, undefined);
return stalledStop.promise;
});
const closing = yield* Scope.close(viewerScope, Exit.void).pipe(Effect.forkScoped);
yield* Queue.take(stopped);
yield* TestClock.adjust("5 seconds");
const detached = session.detach.mock.calls.length;
stalledStop.resolve({});
yield* Fiber.join(closing);
expect(detached).toBe(1);
}),
).pipe(Effect.provide(layer)),
);

const certificateError = {
url: "https://device.local/",
code: -202,
Expand Down
33 changes: 21 additions & 12 deletions apps/server/src/preview/ServerBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ const AGENT_CURSOR_MOVE_MS = 160;
const AGENT_CURSOR_CLICK_LEAD_MS = 40;
/** A dialog blocks page script, so a desktop size read gives up after this. */
const DESKTOP_VIEWPORT_READ_TIMEOUT = "1 second";
const VIEWER_COMMAND_TIMEOUT = "5 seconds";

const sleepUntil = (deadline: number) =>
new Promise<void>((resolve) => setTimeout(resolve, Math.max(0, deadline - Date.now())));
Expand Down Expand Up @@ -1805,6 +1806,14 @@ const make = Effect.gen(function* () {
return started;
};

const withinViewerDeadline = <A>(command: Promise<A>): Promise<A | null> =>
runPromise(
Effect.promise(() => command.catch(() => null)).pipe(
Effect.timeoutOption(VIEWER_COMMAND_TIMEOUT),
Effect.map(Option.getOrNull),
),
);

const withScreencastsPaused = <A>(tab: ServerTab, capture: () => Promise<A>): Promise<A> =>
withCaptureLock(tab, async () => {
tab.capturing += 1;
Expand Down Expand Up @@ -2569,9 +2578,7 @@ const make = Effect.gen(function* () {
}),
(session) =>
Effect.promise(() =>
session
.send("Page.stopScreencast")
.catch(constVoid)
withinViewerDeadline(session.send("Page.stopScreencast"))
.then(() => session.detach())
.catch(constVoid),
),
Expand All @@ -2588,15 +2595,15 @@ const make = Effect.gen(function* () {
screencastScale = scale;
screencastParams = screencastParams.then(async () => {
if (tab.capturing > 0) return;
await session.send("Page.stopScreencast").catch(constVoid);
await session
.send("Page.startScreencast", {
await withinViewerDeadline(session.send("Page.stopScreencast"));
await withinViewerDeadline(
session.send("Page.startScreencast", {
format: "jpeg",
quality: screencastScale < 1 ? Math.min(quality, SCREENCAST_MOTION_QUALITY) : quality,
maxWidth: Math.max(1, Math.round(input.maxWidth * screencastScale)),
maxHeight: Math.max(1, Math.round(input.maxHeight * screencastScale)),
})
.catch(constVoid);
}),
);
});
return screencastParams;
};
Expand Down Expand Up @@ -2629,9 +2636,9 @@ const make = Effect.gen(function* () {
}
},
pause: () => {
screencastParams = screencastParams.then(() =>
session.send("Page.stopScreencast").then(constVoid, constVoid),
);
screencastParams = screencastParams
.then(() => withinViewerDeadline(session.send("Page.stopScreencast")))
.then(constVoid);
return screencastParams;
},
resume: () => startScreencast(screencastScale),
Expand All @@ -2658,7 +2665,9 @@ const make = Effect.gen(function* () {
pushFileChooser(tab);
const pushStill = async () => {
const data = await withCaptureLock(tab, () =>
ServerBrowserPage.captureViewport(session, { format: "jpeg", quality }),
withinViewerDeadline(
ServerBrowserPage.captureViewport(session, { format: "jpeg", quality }),
),
).catch(() => null);
if (data) viewer.push({ _tag: "frame", data: Buffer.from(data, "base64"), ack: Effect.void });
};
Expand Down
Loading