@expo/serve-sim 0.13.1 → 0.13.2
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/dist/middleware.js +125 -125
- package/dist/native/serve-sim-native.node +0 -0
- package/dist/serve-sim.js +128 -128
- package/package.json +1 -1
- package/src/middleware.ts +71 -11
- package/src/native.ts +5 -0
package/package.json
CHANGED
package/src/middleware.ts
CHANGED
|
@@ -3,8 +3,10 @@ export { startDeviceInProcess } from "./device-lifecycle";
|
|
|
3
3
|
export { gridDeviceState } from "./state";
|
|
4
4
|
import { SingleDevicePolicy } from "./single-device";
|
|
5
5
|
import { getScreenshotDisplayIdsAsync } from "./screenshot-display";
|
|
6
|
+
import { ScreenshotParams } from "./host-actions";
|
|
6
7
|
import {
|
|
7
8
|
captureScreenshotFileAsync,
|
|
9
|
+
captureNativeScreenshotAsync,
|
|
8
10
|
ScreenshotDisplay,
|
|
9
11
|
SCREENSHOT_DISPLAY_CHANGED,
|
|
10
12
|
} from "./screenshot-store";
|
|
@@ -2128,6 +2130,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2128
2130
|
const ALWAYS_GATED_PREFIX = "/network-capture";
|
|
2129
2131
|
|
|
2130
2132
|
const connectMiddleware = (async (req: SimReq, res: SimRes, next?: SimNext) => {
|
|
2133
|
+
const requestStarted = performance.now();
|
|
2131
2134
|
const rawUrl: string = req.url ?? "";
|
|
2132
2135
|
const qIndex = rawUrl.indexOf("?");
|
|
2133
2136
|
const url = qIndex === -1 ? rawUrl : rawUrl.slice(0, qIndex);
|
|
@@ -2797,7 +2800,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2797
2800
|
return;
|
|
2798
2801
|
}
|
|
2799
2802
|
|
|
2800
|
-
// Still-PNG capture
|
|
2803
|
+
// Still-PNG capture from native pixels (explicit displays use simctl). Consumed by the
|
|
2801
2804
|
// Expo Device Hub dashboard's save-screenshot action and by the serve-sim web
|
|
2802
2805
|
// UI when it is served through a tunnel (on loopback it uses the
|
|
2803
2806
|
// screenshot.capture host action instead). Uses the
|
|
@@ -2842,18 +2845,46 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2842
2845
|
}
|
|
2843
2846
|
udid = udid.toUpperCase();
|
|
2844
2847
|
// HTTP downloads avoid staging and Desktop copies, but share panel election.
|
|
2845
|
-
|
|
2848
|
+
let file: string | undefined;
|
|
2846
2849
|
try {
|
|
2850
|
+
const captureStarted = performance.now();
|
|
2851
|
+
let nativeDuration = 0;
|
|
2847
2852
|
const screenId = display?.data;
|
|
2848
|
-
const capture = (id: number
|
|
2849
|
-
|
|
2850
|
-
|
|
2853
|
+
const capture = async (id: number) => {
|
|
2854
|
+
file ??= join(tmpdir(), `serve-sim-screenshot-${randomBytes(8).toString("hex")}.png`);
|
|
2855
|
+
const result = await captureScreenshotFileAsync({
|
|
2856
|
+
udid,
|
|
2857
|
+
file,
|
|
2858
|
+
display: id,
|
|
2859
|
+
timeoutMs: 5_000,
|
|
2860
|
+
});
|
|
2861
|
+
if (result.exitCode !== 0)
|
|
2862
|
+
throw new Error(result.stderr || "Simulator screenshot failed");
|
|
2863
|
+
return await readFile(file);
|
|
2864
|
+
};
|
|
2865
|
+
const png =
|
|
2851
2866
|
screenId !== undefined
|
|
2852
2867
|
? await capture(screenId)
|
|
2853
2868
|
: await withScreenshotDeviceSession(udid, streamSettings, async (session) => {
|
|
2854
2869
|
const read = () => session.activeScreenId();
|
|
2855
|
-
return
|
|
2870
|
+
return Buffer.from(
|
|
2871
|
+
await captureNativeScreenshotAsync(
|
|
2872
|
+
await read(),
|
|
2873
|
+
read,
|
|
2874
|
+
async (id, timeoutMs) => {
|
|
2875
|
+
const started = performance.now();
|
|
2876
|
+
try {
|
|
2877
|
+
return await session.screenshot(id, timeoutMs);
|
|
2878
|
+
} finally {
|
|
2879
|
+
nativeDuration += performance.now() - started;
|
|
2880
|
+
}
|
|
2881
|
+
},
|
|
2882
|
+
5_000,
|
|
2883
|
+
),
|
|
2884
|
+
);
|
|
2856
2885
|
}).catch(async (error) => {
|
|
2886
|
+
if (error instanceof Error && error.message === SCREENSHOT_DISPLAY_CHANGED)
|
|
2887
|
+
throw error;
|
|
2857
2888
|
// A sole integrated display needs no native panel election. Never
|
|
2858
2889
|
// fall back on an unknown profile or a multi-display device.
|
|
2859
2890
|
if (!(await getBootedUdids())?.has(udid)) throw error;
|
|
@@ -2861,15 +2892,21 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2861
2892
|
if (ids?.length !== 1) throw error;
|
|
2862
2893
|
return await capture(ids[0]!);
|
|
2863
2894
|
});
|
|
2864
|
-
|
|
2865
|
-
const
|
|
2895
|
+
const captureDuration = performance.now() - captureStarted;
|
|
2896
|
+
const artifactStarted = performance.now();
|
|
2866
2897
|
const artifact = await saveScreenshotArtifact(png);
|
|
2867
2898
|
recordScreenshotEvent(udid, artifact);
|
|
2899
|
+
const artifactDuration = performance.now() - artifactStarted;
|
|
2900
|
+
// Capture includes native queueing/PNG encoding; simctl also uses temporary-file I/O.
|
|
2901
|
+
// Total stops before response delivery; durations overlap, so do not add them.
|
|
2902
|
+
const serverTiming = `total;dur=${(performance.now() - requestStarted).toFixed(3)}, capture;dur=${captureDuration.toFixed(3)}, native;dur=${nativeDuration.toFixed(3)}, artifact;dur=${artifactDuration.toFixed(3)}`;
|
|
2868
2903
|
res.writeHead(200, {
|
|
2869
2904
|
"Cache-Control": "no-store",
|
|
2870
2905
|
"Content-Type": "image/png",
|
|
2906
|
+
"Content-Length": String(png.byteLength),
|
|
2871
2907
|
// The route allows configured cross-origin callers, and they can only read custom headers listed here.
|
|
2872
|
-
"Access-Control-Expose-Headers": `${SCREENSHOT_ARTIFACT_HEADER}, ${SCREENSHOT_ARTIFACT_ERROR_HEADER}`,
|
|
2908
|
+
"Access-Control-Expose-Headers": `${SCREENSHOT_ARTIFACT_HEADER}, ${SCREENSHOT_ARTIFACT_ERROR_HEADER}, Server-Timing`,
|
|
2909
|
+
"Server-Timing": serverTiming,
|
|
2873
2910
|
...screenshotArtifactHeaders(artifact),
|
|
2874
2911
|
});
|
|
2875
2912
|
res.end(png);
|
|
@@ -2879,7 +2916,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2879
2916
|
(typeof stderr === "string" && stderr.trim()) ||
|
|
2880
2917
|
(err instanceof Error ? err.message : String(err));
|
|
2881
2918
|
// The event log is browser-readable; simctl's stderr can name host paths, so it stays here.
|
|
2882
|
-
console.error(`
|
|
2919
|
+
console.error(`Simulator screenshot failed for ${udid}:`, message);
|
|
2883
2920
|
recordScreenshotEvent(udid, {
|
|
2884
2921
|
status: "capture-failed",
|
|
2885
2922
|
error: "simctl screenshot failed",
|
|
@@ -2899,7 +2936,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2899
2936
|
);
|
|
2900
2937
|
} finally {
|
|
2901
2938
|
// Best-effort cleanup; the PNG is already in memory by now.
|
|
2902
|
-
await unlink(file).catch(() => {});
|
|
2939
|
+
if (file) await unlink(file).catch(() => {});
|
|
2903
2940
|
}
|
|
2904
2941
|
return;
|
|
2905
2942
|
}
|
|
@@ -3449,6 +3486,29 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
3449
3486
|
corsOrigins,
|
|
3450
3487
|
ssePrefixes: sseStreamPaths(base),
|
|
3451
3488
|
onUiRequest: handleUiRequest,
|
|
3489
|
+
async onScreenshotRequest(payload, request) {
|
|
3490
|
+
const parsed = ScreenshotParams.omit({ fileName: true })
|
|
3491
|
+
.partial({ udid: true })
|
|
3492
|
+
.strict()
|
|
3493
|
+
.safeParse(payload);
|
|
3494
|
+
if (!parsed.success)
|
|
3495
|
+
return Response.json({ error: "Invalid screenshot request" }, { status: 400 });
|
|
3496
|
+
const url = new URL(`${base}/api/screenshot`, request.url);
|
|
3497
|
+
if (parsed.data.udid !== undefined) {
|
|
3498
|
+
const { resolveBootedDeviceAsync } = await import("./device");
|
|
3499
|
+
// Enforce the same target policy as ordinary actions before resolving aliases.
|
|
3500
|
+
policy?.resolveTarget(parsed.data.udid);
|
|
3501
|
+
url.searchParams.set("device", await resolveBootedDeviceAsync(parsed.data.udid));
|
|
3502
|
+
}
|
|
3503
|
+
if (parsed.data.display !== undefined)
|
|
3504
|
+
url.searchParams.set("display", String(parsed.data.display));
|
|
3505
|
+
return (await fetchMiddleware(
|
|
3506
|
+
new Request(url, {
|
|
3507
|
+
method: "POST",
|
|
3508
|
+
headers: { authorization: `Bearer ${execToken}` },
|
|
3509
|
+
}),
|
|
3510
|
+
))!;
|
|
3511
|
+
},
|
|
3452
3512
|
get unavailable() {
|
|
3453
3513
|
return configuringSession || awaitingAdoption;
|
|
3454
3514
|
},
|
package/src/native.ts
CHANGED
|
@@ -61,6 +61,7 @@ interface SimHIDHandle {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
interface SimCaptureHandle {
|
|
64
|
+
screenshot(screenID: number): Promise<Uint8Array>;
|
|
64
65
|
start(): Promise<void>;
|
|
65
66
|
suspendForReboot(): Promise<void>;
|
|
66
67
|
resumeAfterReboot(): Promise<void>;
|
|
@@ -394,6 +395,10 @@ export class NativeHid {
|
|
|
394
395
|
* thread after being marshalled from the native encode thread.
|
|
395
396
|
*/
|
|
396
397
|
export class NativeCapture {
|
|
398
|
+
screenshot(screenID: number): Promise<Uint8Array> {
|
|
399
|
+
return this.handle.screenshot(screenID);
|
|
400
|
+
}
|
|
401
|
+
|
|
397
402
|
private readonly handle: SimCaptureHandle;
|
|
398
403
|
|
|
399
404
|
/** A fixed screen ID captures that panel independently of active-display/input routing. */
|