@expo/serve-sim 0.10.3 → 0.10.4
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 +132 -132
- package/dist/native/serve-sim-native.node +0 -0
- package/dist/serve-sim.js +156 -156
- package/package.json +1 -1
- package/src/middleware.ts +76 -17
- package/src/native.ts +14 -5
package/package.json
CHANGED
package/src/middleware.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { getScreenshotDisplayIdsAsync } from "./screenshot-display";
|
|
2
|
+
import {
|
|
3
|
+
captureScreenshotFileAsync,
|
|
4
|
+
ScreenshotDisplay,
|
|
5
|
+
SCREENSHOT_DISPLAY_CHANGED,
|
|
6
|
+
} from "./screenshot-store";
|
|
1
7
|
import { openSseStream } from "./sse-stream";
|
|
2
8
|
import { parseCaptureProxy } from "./capture/upstream";
|
|
3
9
|
import { execFile, execSync } from "child_process";
|
|
@@ -46,7 +52,14 @@ import {
|
|
|
46
52
|
type ForegroundTrackerCache,
|
|
47
53
|
} from "./foreground-tracker";
|
|
48
54
|
import { corsAllowOriginHeaders, frameAncestorsPolicy, isAllowedOrigin } from "./middleware-utils";
|
|
49
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
closeDeviceSession,
|
|
57
|
+
getDeviceSession,
|
|
58
|
+
withScreenshotDeviceSession,
|
|
59
|
+
peekDeviceSession,
|
|
60
|
+
retainDeviceSession,
|
|
61
|
+
withDeviceSessionTask,
|
|
62
|
+
} from "./device-session";
|
|
50
63
|
import {
|
|
51
64
|
assertBearerAccess,
|
|
52
65
|
assertCaptureAccess,
|
|
@@ -766,8 +779,29 @@ function serveHelperInProcess(
|
|
|
766
779
|
void live.handlePanel(req, res, Number(panelRoute[1]), panelRoute[2]!);
|
|
767
780
|
return true;
|
|
768
781
|
}
|
|
782
|
+
// Unknown and one-shot helper reads must not claim screenshot-owned capture.
|
|
783
|
+
if (
|
|
784
|
+
!panelRoute &&
|
|
785
|
+
![
|
|
786
|
+
"/stream.mjpeg",
|
|
787
|
+
"/stream.avcc",
|
|
788
|
+
"/stream-settings",
|
|
789
|
+
"/config",
|
|
790
|
+
"/health",
|
|
791
|
+
"/webrtc/offer",
|
|
792
|
+
"/webrtc/close",
|
|
793
|
+
"/recording/video",
|
|
794
|
+
"/ax",
|
|
795
|
+
"/foreground",
|
|
796
|
+
].includes(endpoint ?? "")
|
|
797
|
+
)
|
|
798
|
+
return false;
|
|
799
|
+
const retainsCapture =
|
|
800
|
+
Boolean(panelRoute) ||
|
|
801
|
+
((endpoint === "/stream.mjpeg" || endpoint === "/stream.avcc") && req.method === "GET") ||
|
|
802
|
+
((endpoint === "/webrtc/offer" || endpoint === "/recording/video") && req.method === "POST");
|
|
769
803
|
try {
|
|
770
|
-
session = getDeviceSession(device, initialStreamSettings);
|
|
804
|
+
session = getDeviceSession(device, initialStreamSettings, retainsCapture);
|
|
771
805
|
} catch {
|
|
772
806
|
return false; // not booted / capture unavailable → 404
|
|
773
807
|
}
|
|
@@ -783,7 +817,9 @@ function serveHelperInProcess(
|
|
|
783
817
|
session.handleAvcc(req, res);
|
|
784
818
|
return true;
|
|
785
819
|
case "/stream-settings":
|
|
786
|
-
void session
|
|
820
|
+
void withDeviceSessionTask(session, async () => {
|
|
821
|
+
if (await session.handleStreamSettings(req, res)) retainDeviceSession(session);
|
|
822
|
+
});
|
|
787
823
|
return true;
|
|
788
824
|
case "/config":
|
|
789
825
|
session.handleConfig(req, res);
|
|
@@ -2765,21 +2801,36 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2765
2801
|
res.end(JSON.stringify({ ok: false, error: "No booted simulator to screenshot" }));
|
|
2766
2802
|
return;
|
|
2767
2803
|
}
|
|
2768
|
-
|
|
2769
|
-
|
|
2804
|
+
const displayValue = new URL(rawUrl, "http://localhost").searchParams.get("display");
|
|
2805
|
+
const display =
|
|
2806
|
+
displayValue === null ? undefined : ScreenshotDisplay.safeParse(Number(displayValue));
|
|
2807
|
+
if (display && !display.success) {
|
|
2808
|
+
res.writeHead(400, { "Cache-Control": "no-store", "Content-Type": "application/json" });
|
|
2809
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid simulator display ID" }));
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
udid = udid.toUpperCase();
|
|
2813
|
+
// HTTP downloads avoid staging and Desktop copies, but share panel election.
|
|
2770
2814
|
const file = join(tmpdir(), `serve-sim-screenshot-${randomBytes(8).toString("hex")}.png`);
|
|
2771
2815
|
try {
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2816
|
+
const screenId = display?.data;
|
|
2817
|
+
const capture = (id: number, read?: () => Promise<number>) =>
|
|
2818
|
+
captureScreenshotFileAsync({ udid, file, display: id, timeoutMs: 5_000 }, read);
|
|
2819
|
+
const result =
|
|
2820
|
+
screenId !== undefined
|
|
2821
|
+
? await capture(screenId)
|
|
2822
|
+
: await withScreenshotDeviceSession(udid, streamSettings, async (session) => {
|
|
2823
|
+
const read = () => session.activeScreenId();
|
|
2824
|
+
return await capture(await read(), read);
|
|
2825
|
+
}).catch(async (error) => {
|
|
2826
|
+
// A sole integrated display needs no native panel election. Never
|
|
2827
|
+
// fall back on an unknown profile or a multi-display device.
|
|
2828
|
+
if (!(await getBootedUdids())?.has(udid)) throw error;
|
|
2829
|
+
const ids = await getScreenshotDisplayIdsAsync(udid);
|
|
2830
|
+
if (ids?.length !== 1) throw error;
|
|
2831
|
+
return await capture(ids[0]!);
|
|
2832
|
+
});
|
|
2833
|
+
if (result.exitCode !== 0) throw new Error(result.stderr || "simctl screenshot failed");
|
|
2783
2834
|
const png = await readFile(file);
|
|
2784
2835
|
const artifact = await saveScreenshotArtifact(png);
|
|
2785
2836
|
recordScreenshotEvent(udid, artifact);
|
|
@@ -2806,7 +2857,15 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2806
2857
|
"Cache-Control": "no-store",
|
|
2807
2858
|
"Content-Type": "application/json",
|
|
2808
2859
|
});
|
|
2809
|
-
res.end(
|
|
2860
|
+
res.end(
|
|
2861
|
+
JSON.stringify({
|
|
2862
|
+
ok: false,
|
|
2863
|
+
error:
|
|
2864
|
+
message === SCREENSHOT_DISPLAY_CHANGED
|
|
2865
|
+
? SCREENSHOT_DISPLAY_CHANGED
|
|
2866
|
+
: "Simulator screenshot failed",
|
|
2867
|
+
}),
|
|
2868
|
+
);
|
|
2810
2869
|
} finally {
|
|
2811
2870
|
// Best-effort cleanup; the PNG is already in memory by now.
|
|
2812
2871
|
await unlink(file).catch(() => {});
|
package/src/native.ts
CHANGED
|
@@ -25,7 +25,7 @@ interface SimHIDHandle {
|
|
|
25
25
|
setScreen(screenId: number): Promise<boolean | void>;
|
|
26
26
|
supportsHingeAngle(): Promise<boolean>;
|
|
27
27
|
hingeState(): Promise<NativeHingeState>;
|
|
28
|
-
hingeControlEligibility(): Promise<
|
|
28
|
+
hingeControlEligibility(): Promise<NativeHingeReadback>;
|
|
29
29
|
setHingeAngle(angle: number): Promise<boolean>;
|
|
30
30
|
setHingePose(pose: string): Promise<boolean>;
|
|
31
31
|
supportsPhysicalOrientation(): Promise<boolean>;
|
|
@@ -77,6 +77,7 @@ interface SimCaptureHandle {
|
|
|
77
77
|
closeWebRTCSession(sessionId: string): Promise<void>;
|
|
78
78
|
webRTCSenderStats(sessionId: string): Promise<string>;
|
|
79
79
|
screenSize(): Promise<NativeScreenInfo>;
|
|
80
|
+
activeScreenId(): Promise<number>;
|
|
80
81
|
subscribeScreenChanges(onChange: () => Promise<void>): Promise<NativeUnsubscribe>;
|
|
81
82
|
subscribeDeviceState(onSnapshot: (json: string) => Promise<void>): Promise<NativeUnsubscribe>;
|
|
82
83
|
stop(): Promise<void>;
|
|
@@ -136,6 +137,9 @@ export type NativeHingeState = {
|
|
|
136
137
|
physicalOrientation?: HingePhysicalOrientation;
|
|
137
138
|
};
|
|
138
139
|
|
|
140
|
+
/** Command reconciliation never synthesizes native tabletop activity. */
|
|
141
|
+
export type NativeHingeReadback = NativeHingeState & { tableModeActive?: boolean };
|
|
142
|
+
|
|
139
143
|
export type NativeDeviceStateSnapshot = {
|
|
140
144
|
generation: number;
|
|
141
145
|
revision: number;
|
|
@@ -289,13 +293,14 @@ export class NativeHid {
|
|
|
289
293
|
return this.guard("hingeState", () => this.handle.hingeState(), {});
|
|
290
294
|
}
|
|
291
295
|
|
|
292
|
-
async hingeControlEligibility(): Promise<
|
|
296
|
+
async hingeControlEligibility(): Promise<NativeHingeReadback | null> {
|
|
297
|
+
// An empty sample has unknown fields; null means the read failed.
|
|
293
298
|
return (
|
|
294
|
-
(await this.guard(
|
|
299
|
+
(await this.guard<NativeHingeReadback | null>(
|
|
295
300
|
"hingeControlEligibility",
|
|
296
301
|
() => this.handle.hingeControlEligibility(),
|
|
297
|
-
|
|
298
|
-
)) ??
|
|
302
|
+
null,
|
|
303
|
+
)) ?? null
|
|
299
304
|
);
|
|
300
305
|
}
|
|
301
306
|
|
|
@@ -484,6 +489,10 @@ export class NativeCapture {
|
|
|
484
489
|
return this.handle.screenSize();
|
|
485
490
|
}
|
|
486
491
|
|
|
492
|
+
activeScreenId(): Promise<number> {
|
|
493
|
+
return this.handle.activeScreenId();
|
|
494
|
+
}
|
|
495
|
+
|
|
487
496
|
async subscribeDeviceState(
|
|
488
497
|
onSnapshot: (snapshot: NativeDeviceStateSnapshot) => Promise<void>,
|
|
489
498
|
): Promise<NativeUnsubscribe> {
|