serve-sim 0.1.49 → 0.1.50-beta.145.1
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/README.md +3 -1
- package/Sources/SimCameraInjector/SimCamFakes.h +1 -0
- package/Sources/SimCameraInjector/SimCamFakes.m +15 -0
- package/Sources/SimCameraInjector/SimCamSwizzles.m +17 -13
- package/Sources/SimDuoHID/main.m +53 -1
- package/dist/middleware.js +46 -46
- package/dist/serve-sim.js +70 -70
- package/dist/simcam/libSimCameraInjector.dylib +0 -0
- package/dist/simduo/serve-sim-duo-hid +0 -0
- package/package.json +1 -1
- package/src/device-session.ts +2 -1
- package/src/duo-state.ts +53 -9
- package/src/native.ts +6 -1
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/src/device-session.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
Orientation,
|
|
29
29
|
axDescribeAsync,
|
|
30
30
|
axFrontmostAsync,
|
|
31
|
+
duoHidHelperPath,
|
|
31
32
|
type MjpegFrame,
|
|
32
33
|
} from "./native";
|
|
33
34
|
import { createReadStream } from "fs";
|
|
@@ -311,7 +312,7 @@ export class DeviceSession {
|
|
|
311
312
|
if (!this.dependencies?.createDuoMonitor) {
|
|
312
313
|
void warnDeviceHubInput(this.udid).then((shadowed) => { this.deviceHubShadowed = shadowed; });
|
|
313
314
|
}
|
|
314
|
-
const createMonitor = this.dependencies?.createDuoMonitor ?? ((udid, onState) => new DuoStateMonitor(udid, onState));
|
|
315
|
+
const createMonitor = this.dependencies?.createDuoMonitor ?? ((udid, onState) => new DuoStateMonitor(udid, onState, { duoHelper: duoHidHelperPath }));
|
|
315
316
|
this.duoMonitor = createMonitor(this.udid, (state) => {
|
|
316
317
|
void this.followDuoState(state).catch((error) => {
|
|
317
318
|
console.error("[duo] State update failed:", error);
|
package/src/duo-state.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { execFile, spawn, type ChildProcess } from "node:child_process";
|
|
|
2
2
|
import type { SimulatorOrientation } from "./client/types";
|
|
3
3
|
import { simctlArguments } from "./simctl";
|
|
4
4
|
|
|
5
|
+
/** One `angle <degrees>` line from `serve-sim-duo-hid watch-angle`. */
|
|
5
6
|
export function parseHingeSample(line: string): number | null {
|
|
6
|
-
|
|
7
|
-
const match = /\bAngle:\s*(-?[\d.]+)°/.exec(line);
|
|
7
|
+
const match = /^angle (\d+(?:\.\d+)?)$/.exec(line.trim());
|
|
8
8
|
const angle = match ? Number(match[1]) : NaN;
|
|
9
9
|
return Number.isFinite(angle) && angle >= 0 && angle <= 180 ? angle : null;
|
|
10
10
|
}
|
|
@@ -40,6 +40,18 @@ export interface DuoState {
|
|
|
40
40
|
orientations: Partial<Record<"cover" | "inner", SimulatorOrientation>>;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export interface DuoStateMonitorOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Path of the guest helper (`serve-sim-duo-hid`). Resolved when the hinge
|
|
46
|
+
* reader starts; if it throws, the hinge stays unknown and the rest of the
|
|
47
|
+
* monitor keeps running.
|
|
48
|
+
*/
|
|
49
|
+
duoHelper: () => string;
|
|
50
|
+
/** Process launchers; tests substitute fakes. */
|
|
51
|
+
spawn?: typeof spawn;
|
|
52
|
+
execFile?: typeof execFile;
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
/** One shared monitor per device session; phones never start these processes. */
|
|
44
56
|
export class DuoStateMonitor {
|
|
45
57
|
private stopped = false;
|
|
@@ -51,8 +63,17 @@ export class DuoStateMonitor {
|
|
|
51
63
|
private retry?: ReturnType<typeof setTimeout>;
|
|
52
64
|
private poll?: ReturnType<typeof setTimeout>;
|
|
53
65
|
private state: DuoState = { orientations: {} };
|
|
66
|
+
private hingeFailureLogged = false;
|
|
67
|
+
private readonly spawn: typeof spawn;
|
|
68
|
+
private readonly execFile: typeof execFile;
|
|
54
69
|
|
|
55
|
-
constructor(
|
|
70
|
+
constructor(
|
|
71
|
+
private readonly udid: string,
|
|
72
|
+
private readonly onState: (state: DuoState) => void,
|
|
73
|
+
private readonly options: DuoStateMonitorOptions,
|
|
74
|
+
) {
|
|
75
|
+
this.spawn = options.spawn ?? spawn;
|
|
76
|
+
this.execFile = options.execFile ?? execFile;
|
|
56
77
|
this.watchPanels();
|
|
57
78
|
this.watchHinge();
|
|
58
79
|
this.refreshOrientation();
|
|
@@ -61,7 +82,7 @@ export class DuoStateMonitor {
|
|
|
61
82
|
refreshOrientation(): void {
|
|
62
83
|
if (this.stopped || this.enumeration) return;
|
|
63
84
|
if (this.poll) clearTimeout(this.poll);
|
|
64
|
-
this.enumeration = execFile("/usr/bin/xcrun", simctlArguments(["io", this.udid, "enumerate"]),
|
|
85
|
+
this.enumeration = this.execFile("/usr/bin/xcrun", simctlArguments(["io", this.udid, "enumerate"]),
|
|
65
86
|
{ timeout: 3000, maxBuffer: 1024 * 1024 }, (error, stdout) => {
|
|
66
87
|
this.enumeration = undefined;
|
|
67
88
|
if (this.stopped) return;
|
|
@@ -89,7 +110,7 @@ export class DuoStateMonitor {
|
|
|
89
110
|
this.onState(this.state);
|
|
90
111
|
this.refreshOrientation();
|
|
91
112
|
};
|
|
92
|
-
const child = spawn("/usr/bin/xcrun", simctlArguments(["spawn", this.udid, "log", "stream", "--style", "compact", "--predicate", predicate]), { stdio: ["ignore", "pipe", "ignore"] });
|
|
113
|
+
const child = this.spawn("/usr/bin/xcrun", simctlArguments(["spawn", this.udid, "log", "stream", "--style", "compact", "--predicate", predicate]), { stdio: ["ignore", "pipe", "ignore"] });
|
|
93
114
|
this.panelMonitor = child;
|
|
94
115
|
let pending = "";
|
|
95
116
|
child.stdout.setEncoding("utf8");
|
|
@@ -103,7 +124,7 @@ export class DuoStateMonitor {
|
|
|
103
124
|
}
|
|
104
125
|
});
|
|
105
126
|
// Seed from this boot's most recent transition, then let live events win.
|
|
106
|
-
this.panelSeed = execFile("/usr/bin/xcrun", simctlArguments(["spawn", this.udid, "log", "show", "--last", "boot", "--style", "compact", "--predicate", predicate]),
|
|
127
|
+
this.panelSeed = this.execFile("/usr/bin/xcrun", simctlArguments(["spawn", this.udid, "log", "show", "--last", "boot", "--style", "compact", "--predicate", predicate]),
|
|
107
128
|
{ timeout: 10000, maxBuffer: 4 * 1024 * 1024 }, (error, stdout) => {
|
|
108
129
|
this.panelSeed = undefined;
|
|
109
130
|
if (!error && !liveSample) {
|
|
@@ -122,10 +143,25 @@ export class DuoStateMonitor {
|
|
|
122
143
|
|
|
123
144
|
private watchHinge(): void {
|
|
124
145
|
if (this.stopped) return;
|
|
125
|
-
|
|
126
|
-
|
|
146
|
+
let duoHelper: string;
|
|
147
|
+
try {
|
|
148
|
+
duoHelper = this.options.duoHelper();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error(`[duo] Hinge reader unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Read inside the guest from CoreMotion, the source UIKit's UIHinge reads,
|
|
154
|
+
// so the hinge doesn't depend on devicectl: `devicectl device motion` is
|
|
155
|
+
// missing from devicectl 651.13.5 (Xcode 27.1 27A9275) and later.
|
|
156
|
+
// stdin stays open: the reader exits when it closes.
|
|
157
|
+
const child = this.spawn("/usr/bin/xcrun", simctlArguments(["spawn", this.udid, duoHelper, "watch-angle"]),
|
|
158
|
+
{ stdio: ["pipe", "pipe", "pipe"] });
|
|
127
159
|
this.monitor = child;
|
|
128
160
|
let pending = "";
|
|
161
|
+
let stderr = "";
|
|
162
|
+
child.stdin.on("error", () => {});
|
|
163
|
+
child.stderr.setEncoding("utf8");
|
|
164
|
+
child.stderr.on("data", (chunk: string) => { stderr = (stderr + chunk).slice(-1024); });
|
|
129
165
|
child.stdout.setEncoding("utf8");
|
|
130
166
|
child.stdout.on("data", (chunk: string) => {
|
|
131
167
|
pending += chunk;
|
|
@@ -140,9 +176,16 @@ export class DuoStateMonitor {
|
|
|
140
176
|
}
|
|
141
177
|
});
|
|
142
178
|
child.on("error", () => {}); // close also fires for failed spawns; retry there.
|
|
143
|
-
child.once("close", () => {
|
|
179
|
+
child.once("close", (code, signal) => {
|
|
144
180
|
if (this.monitor === child) this.monitor = undefined;
|
|
145
181
|
if (this.stopped) return;
|
|
182
|
+
// A reader that keeps failing would otherwise leave the hinge unknown
|
|
183
|
+
// without a trace; say why once per session.
|
|
184
|
+
if (!this.hingeFailureLogged) {
|
|
185
|
+
this.hingeFailureLogged = true;
|
|
186
|
+
const reason = stderr.trim() || (signal ? `signal ${signal}` : `exit code ${code}`);
|
|
187
|
+
console.error(`[duo] Hinge reader stopped (${reason}); retrying.`);
|
|
188
|
+
}
|
|
146
189
|
this.retry = setTimeout(() => this.watchHinge(), 1000);
|
|
147
190
|
this.retry.unref();
|
|
148
191
|
});
|
|
@@ -155,6 +198,7 @@ export class DuoStateMonitor {
|
|
|
155
198
|
this.panelSeed?.kill();
|
|
156
199
|
clearTimeout(this.retry);
|
|
157
200
|
clearTimeout(this.poll);
|
|
201
|
+
this.monitor?.stdin?.end();
|
|
158
202
|
this.monitor?.kill();
|
|
159
203
|
this.enumeration?.kill();
|
|
160
204
|
}
|
package/src/native.ts
CHANGED
|
@@ -164,6 +164,11 @@ function resolveAddon(): string {
|
|
|
164
164
|
);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/** The guest-side Duo helper (`simctl spawn`): HID commands and the hinge reader. */
|
|
168
|
+
export function duoHidHelperPath(): string {
|
|
169
|
+
return join(dirname(resolveAddon()), "..", "simduo", "serve-sim-duo-hid");
|
|
170
|
+
}
|
|
171
|
+
|
|
167
172
|
let addon: NativeAddon | undefined;
|
|
168
173
|
function load(): NativeAddon {
|
|
169
174
|
if (!addon) {
|
|
@@ -184,7 +189,7 @@ export class NativeHid {
|
|
|
184
189
|
private readonly keys: KeySequencer;
|
|
185
190
|
|
|
186
191
|
constructor(udid: string) {
|
|
187
|
-
this.handle = new (load().SimHID)(udid,
|
|
192
|
+
this.handle = new (load().SimHID)(udid, duoHidHelperPath());
|
|
188
193
|
this.keys = new KeySequencer(this.handle);
|
|
189
194
|
}
|
|
190
195
|
|