serve-sim 0.1.21 → 0.1.23
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/bin/serve-sim-bin +0 -0
- package/dist/middleware.js +24 -24
- package/dist/serve-sim.js +27 -27
- package/package.json +6 -2
- package/src/ax.ts +2 -2
- package/src/middleware.ts +101 -102
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "serve-sim",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Evan Bacon",
|
|
@@ -51,10 +51,14 @@
|
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/bun": "latest",
|
|
54
|
-
"
|
|
54
|
+
"@types/react": "^19.0.0",
|
|
55
|
+
"@types/react-dom": "^19.0.0",
|
|
56
|
+
"bun-plugin-tailwind": "^0.1.2",
|
|
55
57
|
"preact": "^10.29.1",
|
|
56
58
|
"react": "^19.0.0",
|
|
57
59
|
"react-dom": "^19.0.0",
|
|
60
|
+
"serve-sim-client": "workspace:*",
|
|
61
|
+
"tailwindcss": "^4.1.7",
|
|
58
62
|
"typescript": "^5.7.0"
|
|
59
63
|
},
|
|
60
64
|
"dependencies": {
|
package/src/ax.ts
CHANGED
|
@@ -62,12 +62,12 @@ function normalizeAxTree(roots: RawAxeNode[]): AxSnapshot {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
for (let index = 0; index < node.children.length && elements.length < MAX_ELEMENTS; index++) {
|
|
65
|
-
visit(node.children[index]
|
|
65
|
+
visit(node.children[index]!, `${path}.${index}`);
|
|
66
66
|
}
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
for (let index = 0; index < roots.length && elements.length < MAX_ELEMENTS; index++) {
|
|
70
|
-
visit(roots[index]
|
|
70
|
+
visit(roots[index]!, String(index));
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
return {
|
package/src/middleware.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, existsSync, unlinkSync } from "fs";
|
|
2
|
-
import { execSync, spawn, exec, execFile, type ChildProcess } from "child_process";
|
|
2
|
+
import { execSync, spawn, exec, execFile, type ChildProcess, type ExecException } from "child_process";
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { createServer as createNetServer } from "net";
|
|
6
|
+
import type { IncomingMessage, ServerResponse } from "http";
|
|
6
7
|
import { createAxStreamerCache } from "./ax";
|
|
7
8
|
|
|
9
|
+
type SimReq = IncomingMessage;
|
|
10
|
+
type SimRes = ServerResponse;
|
|
11
|
+
type SimNext = (err?: unknown) => void;
|
|
12
|
+
|
|
8
13
|
// Injected at build time as a base64-encoded string via `define`
|
|
9
14
|
declare const __PREVIEW_HTML_B64__: string;
|
|
10
15
|
const STATE_DIR = join(tmpdir(), "serve-sim");
|
|
@@ -31,6 +36,41 @@ type WebKitBridge = {
|
|
|
31
36
|
releaseHighlight?(targetId?: string): void;
|
|
32
37
|
};
|
|
33
38
|
|
|
39
|
+
type InspectWebKitBridgeTarget = {
|
|
40
|
+
targetId: string;
|
|
41
|
+
title?: string;
|
|
42
|
+
appName?: string;
|
|
43
|
+
url?: string;
|
|
44
|
+
type?: string;
|
|
45
|
+
bundleId?: string;
|
|
46
|
+
inUseByOtherInspector?: boolean;
|
|
47
|
+
source?: { kind?: string; id?: string };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type CdpHttpListEntry = {
|
|
51
|
+
id: string;
|
|
52
|
+
title: string;
|
|
53
|
+
url: string;
|
|
54
|
+
type: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type CdpHttpVersion = { Browser?: string };
|
|
59
|
+
|
|
60
|
+
type SimctlBootedList = {
|
|
61
|
+
devices: Record<string, Array<{ udid: string; state: string }>>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type SimctlAllList = {
|
|
65
|
+
devices: Record<string, Array<Omit<SimctlDevice, "runtime">>>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type ShutdownRequestBody = { udid?: string };
|
|
69
|
+
type StartRequestBody = { udid?: string };
|
|
70
|
+
type ReleaseRequestBody = { targetId?: string };
|
|
71
|
+
type HighlightRequestBody = { targetId?: string; on?: boolean };
|
|
72
|
+
type ExecRequestBody = { command?: string };
|
|
73
|
+
|
|
34
74
|
export interface ServeSimState {
|
|
35
75
|
pid: number;
|
|
36
76
|
port: number;
|
|
@@ -123,53 +163,6 @@ export function matchInstalledAppByDisplayName(
|
|
|
123
163
|
return null;
|
|
124
164
|
}
|
|
125
165
|
|
|
126
|
-
function shellQuote(value: string): string {
|
|
127
|
-
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function installedAppsForDevice(udid: string): Promise<Record<string, InstalledApp>> {
|
|
131
|
-
return new Promise((resolve) => {
|
|
132
|
-
exec(
|
|
133
|
-
`xcrun simctl listapps ${shellQuote(udid)} | plutil -convert json -o - -`,
|
|
134
|
-
{ timeout: 3_000 },
|
|
135
|
-
(err, stdout) => {
|
|
136
|
-
if (err || !stdout) return resolve({});
|
|
137
|
-
try {
|
|
138
|
-
resolve(JSON.parse(stdout) as Record<string, InstalledApp>);
|
|
139
|
-
} catch {
|
|
140
|
-
resolve({});
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
);
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function detectCurrentForegroundApp(
|
|
148
|
-
udid: string,
|
|
149
|
-
stateUrl: string,
|
|
150
|
-
): Promise<{ bundleId: string; isReactNative: boolean } | null> {
|
|
151
|
-
const controller = new AbortController();
|
|
152
|
-
const timer = setTimeout(() => controller.abort(), 2_000);
|
|
153
|
-
try {
|
|
154
|
-
const res = await fetch(`${stateUrl}/ax`, { signal: controller.signal });
|
|
155
|
-
if (!res.ok) return null;
|
|
156
|
-
const tree = await res.json() as Array<{ AXLabel?: unknown }>;
|
|
157
|
-
const displayName = tree?.[0]?.AXLabel;
|
|
158
|
-
if (typeof displayName !== "string") return null;
|
|
159
|
-
|
|
160
|
-
const bundleId = matchInstalledAppByDisplayName(
|
|
161
|
-
await installedAppsForDevice(udid),
|
|
162
|
-
displayName,
|
|
163
|
-
);
|
|
164
|
-
if (!bundleId || !isUserFacingBundle(bundleId)) return null;
|
|
165
|
-
return { bundleId, isReactNative: await detectReactNative(udid, bundleId) };
|
|
166
|
-
} catch {
|
|
167
|
-
return null;
|
|
168
|
-
} finally {
|
|
169
|
-
clearTimeout(timer);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
166
|
// Cache simctl's booted-device set briefly so per-request cost stays bounded.
|
|
174
167
|
// The middleware runs inside the user's dev server (Metro etc.) and
|
|
175
168
|
// readServeSimStates() is called on every /api and every page load.
|
|
@@ -185,9 +178,7 @@ function getBootedUdids(): Set<string> | null {
|
|
|
185
178
|
stdio: ["ignore", "pipe", "pipe"],
|
|
186
179
|
timeout: 3_000,
|
|
187
180
|
});
|
|
188
|
-
const data = JSON.parse(output) as
|
|
189
|
-
devices: Record<string, Array<{ udid: string; state: string }>>;
|
|
190
|
-
};
|
|
181
|
+
const data = JSON.parse(output) as SimctlBootedList;
|
|
191
182
|
const booted = new Set<string>();
|
|
192
183
|
for (const runtime of Object.values(data.devices)) {
|
|
193
184
|
for (const device of runtime) {
|
|
@@ -272,7 +263,7 @@ async function existingInspectWebKitBridge(port: number): Promise<WebKitBridge |
|
|
|
272
263
|
try {
|
|
273
264
|
const versionRes = await fetch(`${cdpUrl}/json/version`);
|
|
274
265
|
if (!versionRes.ok) return null;
|
|
275
|
-
const version = await versionRes.json() as
|
|
266
|
+
const version = await versionRes.json() as CdpHttpVersion;
|
|
276
267
|
if (version.Browser !== "Safari/inspect-webkit") return null;
|
|
277
268
|
return {
|
|
278
269
|
port,
|
|
@@ -283,13 +274,7 @@ async function existingInspectWebKitBridge(port: number): Promise<WebKitBridge |
|
|
|
283
274
|
// shape `sim:<udid>:<appId>:<pageId>` and the description string
|
|
284
275
|
// `<deviceLabel> (<bundleId>)` are all we have here.
|
|
285
276
|
const listRes = await fetch(`${cdpUrl}/json/list`);
|
|
286
|
-
const targets = await listRes.json() as
|
|
287
|
-
id: string;
|
|
288
|
-
title: string;
|
|
289
|
-
url: string;
|
|
290
|
-
type: string;
|
|
291
|
-
description?: string;
|
|
292
|
-
}>;
|
|
277
|
+
const targets = await listRes.json() as CdpHttpListEntry[];
|
|
293
278
|
return targets
|
|
294
279
|
.filter((target) => target.id.startsWith("sim:"))
|
|
295
280
|
.map((target) => {
|
|
@@ -335,29 +320,35 @@ async function ensureInspectWebKitBridge(): Promise<WebKitBridge> {
|
|
|
335
320
|
// (and what the DevTools frontend CSP whitelists). `localhost` resolves
|
|
336
321
|
// to ::1 first on some setups, which would leave the iframe's
|
|
337
322
|
// ws://127.0.0.1:9222 connection refused.
|
|
338
|
-
const server = await startCdpServer({ host: "127.0.0.1", port })
|
|
323
|
+
const server = await startCdpServer({ host: "127.0.0.1", port }) as Awaited<ReturnType<typeof startCdpServer>> & {
|
|
324
|
+
highlightTarget?(targetId: string, on: boolean): Promise<void>;
|
|
325
|
+
releaseHighlight?(targetId?: string): void;
|
|
326
|
+
};
|
|
339
327
|
return {
|
|
340
328
|
port,
|
|
341
329
|
cdpUrl: `http://127.0.0.1:${port}`,
|
|
342
330
|
async listTargets() {
|
|
343
|
-
return server.getTargets()
|
|
344
|
-
.filter((target
|
|
345
|
-
.map((target
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
331
|
+
return (server.getTargets() as InspectWebKitBridgeTarget[])
|
|
332
|
+
.filter((target) => target.source?.kind === "simulator")
|
|
333
|
+
.map((target) => {
|
|
334
|
+
const url = target.url ?? "";
|
|
335
|
+
return {
|
|
336
|
+
id: target.targetId,
|
|
337
|
+
title: target.title || target.appName || url || "Untitled",
|
|
338
|
+
url: /^https?:/i.test(url) ? url : "about:blank",
|
|
339
|
+
type: target.type || "page",
|
|
340
|
+
appName: target.appName,
|
|
341
|
+
bundleId: target.bundleId,
|
|
342
|
+
udid: target.source?.id,
|
|
343
|
+
inUseByOtherInspector: !!target.inUseByOtherInspector,
|
|
344
|
+
};
|
|
345
|
+
});
|
|
355
346
|
},
|
|
356
347
|
highlightTarget: server.highlightTarget?.bind(server),
|
|
357
348
|
releaseHighlight: server.releaseHighlight?.bind(server),
|
|
358
349
|
};
|
|
359
|
-
} catch (err
|
|
360
|
-
if (err?.code === "EADDRINUSE") {
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if ((err as NodeJS.ErrnoException)?.code === "EADDRINUSE") {
|
|
361
352
|
const existing = await existingInspectWebKitBridge(port);
|
|
362
353
|
if (existing) return existing;
|
|
363
354
|
continue;
|
|
@@ -414,9 +405,7 @@ function listAllSimulators(): SimctlDevice[] {
|
|
|
414
405
|
stdio: ["ignore", "pipe", "ignore"],
|
|
415
406
|
timeout: 3_000,
|
|
416
407
|
});
|
|
417
|
-
const data = JSON.parse(output) as
|
|
418
|
-
devices: Record<string, Array<Omit<SimctlDevice, "runtime">>>;
|
|
419
|
-
};
|
|
408
|
+
const data = JSON.parse(output) as SimctlAllList;
|
|
420
409
|
const out: SimctlDevice[] = [];
|
|
421
410
|
for (const [runtime, devices] of Object.entries(data.devices)) {
|
|
422
411
|
// Only iOS (skip watchOS / tvOS / visionOS for the grid MVP — the helper
|
|
@@ -433,10 +422,6 @@ function listAllSimulators(): SimctlDevice[] {
|
|
|
433
422
|
}
|
|
434
423
|
}
|
|
435
424
|
|
|
436
|
-
function deviceNameFor(udid: string): string | null {
|
|
437
|
-
return listAllSimulators().find((d) => d.udid === udid)?.name ?? null;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
425
|
// Default per-simulator footprint when we have no running sim to measure
|
|
441
426
|
// from — a fresh booted iOS sim with one app launched typically sits in
|
|
442
427
|
// the 1.2–1.8 GB range. Used as a fallback only.
|
|
@@ -513,7 +498,7 @@ function readSimulatorMemoryUsage(): { perUdid: Record<string, number>; totalByt
|
|
|
513
498
|
const rssKb = Number(line.split(/\s+/, 1)[0]);
|
|
514
499
|
if (!Number.isFinite(rssKb)) continue;
|
|
515
500
|
const bytes = rssKb * 1024;
|
|
516
|
-
const udid = m[1]
|
|
501
|
+
const udid = m[1]!.toUpperCase();
|
|
517
502
|
perUdid[udid] = (perUdid[udid] ?? 0) + bytes;
|
|
518
503
|
totalBytes += bytes;
|
|
519
504
|
}
|
|
@@ -598,7 +583,7 @@ export interface SimMiddlewareOptions {
|
|
|
598
583
|
export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
599
584
|
const base = (options?.basePath ?? "/.sim").replace(/\/+$/, "");
|
|
600
585
|
|
|
601
|
-
return (req:
|
|
586
|
+
return (req: SimReq, res: SimRes, next?: SimNext) => {
|
|
602
587
|
const rawUrl: string = req.url ?? "";
|
|
603
588
|
const qIndex = rawUrl.indexOf("?");
|
|
604
589
|
const url = qIndex === -1 ? rawUrl : rawUrl.slice(0, qIndex);
|
|
@@ -743,7 +728,7 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
743
728
|
});
|
|
744
729
|
req.on("end", () => {
|
|
745
730
|
let udid = "";
|
|
746
|
-
try { udid = JSON.parse(body).udid ?? ""; } catch {}
|
|
731
|
+
try { udid = (JSON.parse(body) as ShutdownRequestBody).udid ?? ""; } catch {}
|
|
747
732
|
if (!/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i.test(udid)) {
|
|
748
733
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
749
734
|
res.end(JSON.stringify({ ok: false, error: "Invalid or missing udid" }));
|
|
@@ -776,7 +761,7 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
776
761
|
});
|
|
777
762
|
req.on("end", () => {
|
|
778
763
|
let udid = "";
|
|
779
|
-
try { udid = JSON.parse(body).udid ?? ""; } catch {}
|
|
764
|
+
try { udid = (JSON.parse(body) as StartRequestBody).udid ?? ""; } catch {}
|
|
780
765
|
if (!/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i.test(udid)) {
|
|
781
766
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
782
767
|
res.end(JSON.stringify({ ok: false, error: "Invalid or missing udid" }));
|
|
@@ -875,10 +860,10 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
875
860
|
// Optional body { targetId } releases just one; empty body releases all.
|
|
876
861
|
if (url === base + "/devtools/release" && req.method === "POST") {
|
|
877
862
|
let body = "";
|
|
878
|
-
req.on("data", (chunk) => (body += chunk));
|
|
863
|
+
req.on("data", (chunk: Buffer) => (body += chunk));
|
|
879
864
|
req.on("end", async () => {
|
|
880
865
|
try {
|
|
881
|
-
const parsed = body ? JSON.parse(body)
|
|
866
|
+
const parsed: ReleaseRequestBody = body ? JSON.parse(body) : {};
|
|
882
867
|
const bridge = await ensureInspectWebKitBridge();
|
|
883
868
|
bridge.releaseHighlight?.(parsed.targetId);
|
|
884
869
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -898,10 +883,10 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
898
883
|
// { targetId: string, on: boolean }.
|
|
899
884
|
if (url === base + "/devtools/highlight" && req.method === "POST") {
|
|
900
885
|
let body = "";
|
|
901
|
-
req.on("data", (chunk) => (body += chunk));
|
|
886
|
+
req.on("data", (chunk: Buffer) => (body += chunk));
|
|
902
887
|
req.on("end", async () => {
|
|
903
888
|
try {
|
|
904
|
-
const { targetId, on } = JSON.parse(body || "{}") as
|
|
889
|
+
const { targetId, on } = JSON.parse(body || "{}") as HighlightRequestBody;
|
|
905
890
|
if (!targetId) {
|
|
906
891
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
907
892
|
res.end(JSON.stringify({ error: "Missing targetId" }));
|
|
@@ -971,7 +956,7 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
971
956
|
req.on("end", () => {
|
|
972
957
|
let command = "";
|
|
973
958
|
try {
|
|
974
|
-
command = JSON.parse(body).command ?? "";
|
|
959
|
+
command = (JSON.parse(body) as ExecRequestBody).command ?? "";
|
|
975
960
|
} catch {}
|
|
976
961
|
if (!command) {
|
|
977
962
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
@@ -983,7 +968,7 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
983
968
|
res.end(JSON.stringify({
|
|
984
969
|
stdout: stdout.toString(),
|
|
985
970
|
stderr: stderr.toString(),
|
|
986
|
-
exitCode: err ? (err as
|
|
971
|
+
exitCode: err ? (err as ExecException).code ?? 1 : 0,
|
|
987
972
|
}));
|
|
988
973
|
});
|
|
989
974
|
});
|
|
@@ -1051,6 +1036,31 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
1051
1036
|
});
|
|
1052
1037
|
res.write(":\n\n");
|
|
1053
1038
|
|
|
1039
|
+
// Bootstrap: SpringBoard's log feed is edge-triggered, so a fresh
|
|
1040
|
+
// subscriber would otherwise see nothing until the user re-foregrounds
|
|
1041
|
+
// an app (the bug: tools couldn't reconnect after a page reload). Ask
|
|
1042
|
+
// the helper's AX bridge for the current frontmost app via
|
|
1043
|
+
// `proc_pidpath`+Info.plist resolution and emit it before tailing.
|
|
1044
|
+
let lastBundle = "";
|
|
1045
|
+
void (async () => {
|
|
1046
|
+
try {
|
|
1047
|
+
const ctrl = new AbortController();
|
|
1048
|
+
const timer = setTimeout(() => ctrl.abort(), 1500);
|
|
1049
|
+
const r = await fetch(`http://127.0.0.1:${state.port}/foreground`, { signal: ctrl.signal });
|
|
1050
|
+
clearTimeout(timer);
|
|
1051
|
+
if (!r.ok) return;
|
|
1052
|
+
const info = await r.json() as { bundleId?: string; pid?: number };
|
|
1053
|
+
if (!info.bundleId || !isUserFacingBundle(info.bundleId)) return;
|
|
1054
|
+
if (res.writableEnded) return;
|
|
1055
|
+
lastBundle = info.bundleId;
|
|
1056
|
+
const isReactNative = await detectReactNative(udid, info.bundleId);
|
|
1057
|
+
if (res.writableEnded) return;
|
|
1058
|
+
res.write("data: " + JSON.stringify({ bundleId: info.bundleId, pid: info.pid, isReactNative }) + "\n\n");
|
|
1059
|
+
} catch {
|
|
1060
|
+
// Helper may be coming up — log tail will fill in once anything moves.
|
|
1061
|
+
}
|
|
1062
|
+
})();
|
|
1063
|
+
|
|
1054
1064
|
const child: ChildProcess = spawn("xcrun", [
|
|
1055
1065
|
"simctl", "spawn", udid, "log", "stream",
|
|
1056
1066
|
"--style", "ndjson",
|
|
@@ -1059,28 +1069,17 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
1059
1069
|
'process == "SpringBoard" AND eventMessage CONTAINS "Setting process visibility to: Foreground"',
|
|
1060
1070
|
], { stdio: ["ignore", "pipe", "ignore"] });
|
|
1061
1071
|
|
|
1062
|
-
let lastBundle = "";
|
|
1063
|
-
let hasEmitted = false;
|
|
1064
1072
|
let closed = false;
|
|
1065
1073
|
const emitApp = async (bundleId: string, pid?: number) => {
|
|
1066
1074
|
if (!isUserFacingBundle(bundleId)) return;
|
|
1067
1075
|
if (bundleId === lastBundle) return;
|
|
1068
1076
|
lastBundle = bundleId;
|
|
1069
|
-
hasEmitted = true;
|
|
1070
1077
|
const isReactNative = await detectReactNative(udid, bundleId);
|
|
1071
1078
|
if (!closed) {
|
|
1072
1079
|
res.write("data: " + JSON.stringify({ bundleId, pid, isReactNative }) + "\n\n");
|
|
1073
1080
|
}
|
|
1074
1081
|
};
|
|
1075
1082
|
|
|
1076
|
-
// The seed loses to any live log event — a SpringBoard log that fires
|
|
1077
|
-
// while the AX call is in flight is fresher than the AX snapshot.
|
|
1078
|
-
detectCurrentForegroundApp(udid, state.url).then((app) => {
|
|
1079
|
-
if (!app || closed || hasEmitted) return;
|
|
1080
|
-
lastBundle = app.bundleId;
|
|
1081
|
-
hasEmitted = true;
|
|
1082
|
-
res.write("data: " + JSON.stringify(app) + "\n\n");
|
|
1083
|
-
});
|
|
1084
1083
|
|
|
1085
1084
|
let buf = "";
|
|
1086
1085
|
child.stdout!.on("data", (chunk: Buffer) => {
|