@expo/serve-sim 0.2.3 → 0.3.0
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 +51 -1
- package/Sources/ServeSimCapabilityLoader/build.sh +32 -0
- package/Sources/ServeSimCapabilityLoader/serve-sim-capability-loader.c +322 -0
- package/dist/capability-loader/libServeSimCapabilityLoader.dylib +0 -0
- package/dist/middleware.js +153 -80
- package/dist/native/serve-sim-native.node +0 -0
- package/dist/serve-sim.js +201 -123
- package/package.json +7 -1
- package/src/connect-to-fetch.ts +9 -0
- package/src/middleware.ts +78 -32
- package/src/native.ts +70 -3
package/README.md
CHANGED
|
@@ -38,6 +38,26 @@ Requires an Apple silicon (`arm64`) Mac with Xcode command line tools (`xcrun si
|
|
|
38
38
|
|
|
39
39
|
The bundled native addon, simulator tools, and host helpers are arm64-only.
|
|
40
40
|
|
|
41
|
+
The iPhone Duo simulator supports touch input, scrolling, and touch gestures on both the folded cover screen and the half-open or fully open inner screen. Frames and input follow the active display and its orientation; an app can still restrict its supported orientations.
|
|
42
|
+
|
|
43
|
+
Three shortcuts below the phone select Fully folded, Partially open, and Fully open.
|
|
44
|
+
At the top of Simulator settings in the Tools sidebar, the Fold pose dropdown
|
|
45
|
+
adds Laptop and Tent. The next rows provide a live 0–180° hinge slider with
|
|
46
|
+
decimal angle input and Table Mode, using the same controls as other settings.
|
|
47
|
+
Partially open uses Book: it and Laptop use a 90° hinge angle with different
|
|
48
|
+
physical orientations; Tent uses 80°.
|
|
49
|
+
Duo defaults to 3D using Apple's model from the host's Xcode installation,
|
|
50
|
+
with live displays and smooth folding, unfolding, and pose transitions.
|
|
51
|
+
Display pixels stay attached to their
|
|
52
|
+
physical panels as the model moves. Closed and Open face the viewer directly,
|
|
53
|
+
with the open model following the screen's portrait or landscape layout. Laptop
|
|
54
|
+
rests level and Tent presents the outside cover screen. The original `V68.usdz`
|
|
55
|
+
is loaded locally, without bundling Apple's model or downloading it. The view
|
|
56
|
+
respects the browser's reduced-motion preference. **Simulator → Preview mode**
|
|
57
|
+
switches between 2D and 3D; AX inspection also uses the flat view. See [hinge controls and display
|
|
58
|
+
selection](packages/serve-sim/docs/hinge-controls.md) for Device Hub's hidden
|
|
59
|
+
controls and the simulator APIs behind them.
|
|
60
|
+
|
|
41
61
|
## CLI
|
|
42
62
|
|
|
43
63
|
```
|
|
@@ -50,6 +70,8 @@ serve-sim type <text> [-d udid] Type text via the simulator keyboard
|
|
|
50
70
|
serve-sim rotate <orientation> [-d udid]
|
|
51
71
|
portrait | portrait_upside_down |
|
|
52
72
|
landscape_left | landscape_right
|
|
73
|
+
serve-sim hinge <fold|half|unfold|degrees> [-d udid]
|
|
74
|
+
Set the hinge angle (0° folded, 180° unfolded)
|
|
53
75
|
serve-sim ca-debug <option> <on|off> [-d udid]
|
|
54
76
|
Toggle a CoreAnimation debug flag
|
|
55
77
|
(blended|copies|misaligned|offscreen|slow-animations)
|
|
@@ -95,6 +117,13 @@ Options:
|
|
|
95
117
|
H.264/WebRTC target bitrate
|
|
96
118
|
--video-fps <fps>
|
|
97
119
|
H.264/WebRTC frame rate (1-140)
|
|
120
|
+
--launch-app-identifier <id>
|
|
121
|
+
Bundle identifier of an installed app to launch once the
|
|
122
|
+
simulator boots
|
|
123
|
+
--launch-arg <arg>
|
|
124
|
+
Argument passed to the app when it launches (repeatable)
|
|
125
|
+
--open-url <url>
|
|
126
|
+
URL to open in the app after it launches
|
|
98
127
|
--list [device] List running streams
|
|
99
128
|
--kill [device] Kill running stream(s)
|
|
100
129
|
|
|
@@ -171,6 +200,26 @@ Supervisors can probe `GET /healthz` to confirm that the preview server is
|
|
|
171
200
|
listening and `GET /readyz` to wait until the selected simulator and native
|
|
172
201
|
capture session are ready. Both endpoints return JSON and disable caching.
|
|
173
202
|
|
|
203
|
+
### Launching an app
|
|
204
|
+
|
|
205
|
+
`--launch-app-identifier <bundle-id>` launches an app that is already installed on the
|
|
206
|
+
simulator, before the stream starts. Install it yourself first (`xcrun simctl install`);
|
|
207
|
+
`serve-sim` only launches.
|
|
208
|
+
|
|
209
|
+
```sh
|
|
210
|
+
serve-sim --launch-app-identifier host.exp.Exponent \
|
|
211
|
+
--launch-arg -EXDevMenuIsOnboardingFinished --launch-arg 1 \
|
|
212
|
+
--open-url exp://127.0.0.1:8081
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`--launch-arg` is repeatable and maps to `simctl launch` process arguments. `--open-url`
|
|
216
|
+
runs `simctl openurl` after the app is up, and pre-approves custom URL schemes so the
|
|
217
|
+
Simulator does not ask for confirmation. This matters for `exp://` deep links on a headless host.
|
|
218
|
+
|
|
219
|
+
Launching from `serve-sim` rather than beforehand matters because a second `simctl launch`
|
|
220
|
+
on a running app is a no-op: it neither restarts the app nor applies new arguments. Owning
|
|
221
|
+
the launch is what lets `serve-sim` attach to the process from the start.
|
|
222
|
+
|
|
174
223
|
### Camera
|
|
175
224
|
|
|
176
225
|
`serve-sim camera <bundle-id>` replaces the simulator's camera feed for a single app. A small host-side helper writes BGRA frames into a POSIX shared-memory region; an injected dylib (`DYLD_INSERT_LIBRARIES`) swizzles AVFoundation inside the simulator process so the app reads from that region instead of the simulator's stub camera.
|
|
@@ -259,7 +308,7 @@ server.onUpgrade((request, socket) => {
|
|
|
259
308
|
});
|
|
260
309
|
```
|
|
261
310
|
|
|
262
|
-
The middleware reads the helper's state from `$TMPDIR/serve-sim/` and points the browser at the helper's stream, interaction WebSocket, and WebKit DevTools endpoints. By default those URLs target the helper's own port directly (
|
|
311
|
+
The middleware reads the helper's state from `$TMPDIR/serve-sim/` and points the browser at the helper's stream, interaction WebSocket, and WebKit DevTools endpoints. By default those URLs target the helper's own port directly (the helper answers loopback origins), so a plain `app.use(...)` mount works without touching your server's WebSocket handling.
|
|
263
312
|
|
|
264
313
|
### Single-port / remote proxying
|
|
265
314
|
|
|
@@ -296,6 +345,7 @@ The npm package ships the native capture addon and LiveKit WebRTC framework alon
|
|
|
296
345
|
```sh
|
|
297
346
|
bun install
|
|
298
347
|
bun run packages/serve-sim/build.ts # full production build
|
|
348
|
+
bash packages/serve-sim/Sources/build-test-fixtures.sh # simulator test fixtures
|
|
299
349
|
packages/serve-sim/Sources/SimNative/build.sh # native addon only
|
|
300
350
|
bun run --filter @expo/serve-sim dev # watch mode
|
|
301
351
|
bun run --filter @expo/serve-sim tart-dev # guest preview at localhost:3200
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
OUT_DIR="${1:-$HERE/../../dist/capability-loader}"
|
|
6
|
+
mkdir -p "$OUT_DIR"
|
|
7
|
+
|
|
8
|
+
SDK="$(xcrun --sdk iphonesimulator --show-sdk-path)"
|
|
9
|
+
DYLIB="$OUT_DIR/libServeSimCapabilityLoader.dylib"
|
|
10
|
+
|
|
11
|
+
xcrun --sdk iphonesimulator clang \
|
|
12
|
+
-arch arm64 \
|
|
13
|
+
-mios-simulator-version-min=15.0 \
|
|
14
|
+
-isysroot "$SDK" \
|
|
15
|
+
-dynamiclib \
|
|
16
|
+
-O2 \
|
|
17
|
+
-Wall -Wextra -Werror -Wconversion -Wshadow \
|
|
18
|
+
-install_name "@rpath/libServeSimCapabilityLoader.dylib" \
|
|
19
|
+
-o "$DYLIB" \
|
|
20
|
+
"$HERE/serve-sim-capability-loader.c"
|
|
21
|
+
|
|
22
|
+
# This image is inserted into every process in the simulator. Anything beyond
|
|
23
|
+
# libSystem crash-loops system daemons, so fail the build rather than ship it.
|
|
24
|
+
LINKED="$(otool -L "$DYLIB" | grep $'^\t' | awk '{print $1}' | sort -u)"
|
|
25
|
+
UNEXPECTED="$(echo "$LINKED" | grep -v -e '^@rpath/libServeSimCapabilityLoader\.dylib$' -e '^/usr/lib/libSystem\.B\.dylib$' || true)"
|
|
26
|
+
if [ -n "$UNEXPECTED" ]; then
|
|
27
|
+
echo "CapabilityLoader links more than libSystem:" >&2
|
|
28
|
+
echo "$UNEXPECTED" >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
echo "$DYLIB"
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// Inserted into every simulator process via launchd DYLD_INSERT_LIBRARIES, so
|
|
2
|
+
// it links libSystem only: a Foundation-linked insert crash-loops GSSCred, and
|
|
3
|
+
// loading UIKit from the constructor crashes Safari on headless boots. Capability
|
|
4
|
+
// dylibs load on the main queue after the constructor returns.
|
|
5
|
+
//
|
|
6
|
+
// Config format, one capability per line, written by the launch manager:
|
|
7
|
+
// <user|all>\t<dylib>\t[KEY=VALUE;KEY=VALUE]\t<delay-ms>
|
|
8
|
+
//
|
|
9
|
+
// The launch manager sets SERVE_SIM_CAPABILITIES_CONFIG alongside the insert,
|
|
10
|
+
// per simulator, so the config can live with the rest of serve-sim's state
|
|
11
|
+
// rather than inside the installed package.
|
|
12
|
+
|
|
13
|
+
#include <dispatch/dispatch.h>
|
|
14
|
+
#include <dlfcn.h>
|
|
15
|
+
#include <errno.h>
|
|
16
|
+
#include <fcntl.h>
|
|
17
|
+
#include <mach-o/dyld.h>
|
|
18
|
+
#include <sys/stat.h>
|
|
19
|
+
#include <stdio.h>
|
|
20
|
+
#include <stdlib.h>
|
|
21
|
+
#include <string.h>
|
|
22
|
+
#include <unistd.h>
|
|
23
|
+
|
|
24
|
+
#define CONFIG_VAR "SERVE_SIM_CAPABILITIES_CONFIG"
|
|
25
|
+
#define MAX_CONFIG_BYTES (64 * 1024)
|
|
26
|
+
// A capability that links UIKit asks to be loaded late; one that links only
|
|
27
|
+
// libSystem does not have to wait for it.
|
|
28
|
+
#define MAX_LOAD_DELAY_MS 10000
|
|
29
|
+
#define MAX_CAPABILITIES 64
|
|
30
|
+
|
|
31
|
+
// 0 read the whole file, 1 the file did not fit, -1 could not read it.
|
|
32
|
+
static int read_config(const char *path, char *out, size_t cap) {
|
|
33
|
+
int fd;
|
|
34
|
+
do {
|
|
35
|
+
fd = open(path, O_RDONLY | O_NONBLOCK | O_NOFOLLOW);
|
|
36
|
+
} while (fd < 0 && errno == EINTR);
|
|
37
|
+
if (fd < 0) return -1;
|
|
38
|
+
|
|
39
|
+
// Only a real file. A fifo or device would give this thread a read that never ends.
|
|
40
|
+
struct stat info;
|
|
41
|
+
if (fstat(fd, &info) != 0 || !S_ISREG(info.st_mode)) {
|
|
42
|
+
close(fd);
|
|
43
|
+
return -1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
size_t used = 0;
|
|
47
|
+
int truncated = 0;
|
|
48
|
+
for (;;) {
|
|
49
|
+
ssize_t n = read(fd, out + used, cap - 1 - used);
|
|
50
|
+
if (n < 0) {
|
|
51
|
+
if (errno == EINTR) continue;
|
|
52
|
+
close(fd);
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
if (n == 0) break;
|
|
56
|
+
used += (size_t)n;
|
|
57
|
+
if (used >= cap - 1) { truncated = 1; break; }
|
|
58
|
+
}
|
|
59
|
+
close(fd);
|
|
60
|
+
out[used] = '\0';
|
|
61
|
+
return truncated;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static int apply_env(char *pairs) {
|
|
65
|
+
int failed = 0;
|
|
66
|
+
char *pair, *rest = pairs;
|
|
67
|
+
while ((pair = strsep(&rest, ";")) != NULL) {
|
|
68
|
+
if (*pair == '\0') continue;
|
|
69
|
+
char *eq = strchr(pair, '=');
|
|
70
|
+
if (eq == NULL) continue;
|
|
71
|
+
*eq = '\0';
|
|
72
|
+
if (setenv(pair, eq + 1, 1) != 0) {
|
|
73
|
+
fprintf(stderr, "[serve-sim] could not set %s for a capability\n", pair);
|
|
74
|
+
failed = 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return failed;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// An app the user installed lives under the device's own Bundle container; an
|
|
81
|
+
// Apple app ships inside the runtime, under RuntimeRoot.
|
|
82
|
+
#define USER_APP_MARKER "/Containers/Bundle/Application/"
|
|
83
|
+
|
|
84
|
+
static unsigned parse_delay_ms(const char *text) {
|
|
85
|
+
if (text == NULL || *text == '\0') return 0;
|
|
86
|
+
char *end = NULL;
|
|
87
|
+
errno = 0;
|
|
88
|
+
long value = strtol(text, &end, 10);
|
|
89
|
+
if (errno != 0 || end == text || *end != '\0' || value < 0) return 0;
|
|
90
|
+
if (value > MAX_LOAD_DELAY_MS) return MAX_LOAD_DELAY_MS;
|
|
91
|
+
return (unsigned)value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static int capability_applies(char *line, const char *exec_path, char **dylib_out,
|
|
95
|
+
char **env_out, unsigned *delay_ms_out) {
|
|
96
|
+
if (*line == '\0' || *line == '#') return 0;
|
|
97
|
+
|
|
98
|
+
char *fields = line;
|
|
99
|
+
char *scope = strsep(&fields, "\t");
|
|
100
|
+
char *dylib = strsep(&fields, "\t");
|
|
101
|
+
char *env = strsep(&fields, "\t");
|
|
102
|
+
char *delay = strsep(&fields, "\t");
|
|
103
|
+
if (dylib == NULL || *dylib == '\0') return 0;
|
|
104
|
+
if (*dylib != '/') {
|
|
105
|
+
fprintf(stderr, "[serve-sim] ignoring capability path that is not absolute: %s\n", dylib);
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// `all` needs no check of its own: the constructor already refused anything
|
|
110
|
+
// that is not an app. An unknown scope loads nowhere.
|
|
111
|
+
if (strcmp(scope, "user") == 0) {
|
|
112
|
+
if (strstr(exec_path, USER_APP_MARKER) == NULL) return 0;
|
|
113
|
+
} else if (strcmp(scope, "all") != 0) {
|
|
114
|
+
fprintf(stderr, "[serve-sim] ignoring capability with unknown scope '%s': %s\n", scope, dylib);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
*dylib_out = dylib;
|
|
119
|
+
*env_out = env;
|
|
120
|
+
*delay_ms_out = parse_delay_ms(delay);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
struct CapabilityLoad {
|
|
125
|
+
char *dylib;
|
|
126
|
+
char *env;
|
|
127
|
+
unsigned delay_ms;
|
|
128
|
+
uint64_t generation;
|
|
129
|
+
int loaded;
|
|
130
|
+
int pending;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
struct Load {
|
|
134
|
+
char exec_path[1024];
|
|
135
|
+
char config_path[1024];
|
|
136
|
+
uint64_t next_generation;
|
|
137
|
+
struct CapabilityLoad capabilities[MAX_CAPABILITIES];
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
static void load_capabilities(struct Load *load);
|
|
141
|
+
|
|
142
|
+
static void load_one(struct CapabilityLoad *capability) {
|
|
143
|
+
capability->pending = 0;
|
|
144
|
+
char *env = strdup(capability->env);
|
|
145
|
+
if (env == NULL) return;
|
|
146
|
+
int failed = apply_env(env);
|
|
147
|
+
free(env);
|
|
148
|
+
if (failed) {
|
|
149
|
+
fprintf(stderr, "[serve-sim] not loading %s: its environment is incomplete\n",
|
|
150
|
+
capability->dylib);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (dlopen(capability->dylib, RTLD_NOW | RTLD_LOCAL) == NULL) {
|
|
154
|
+
fprintf(stderr, "[serve-sim] could not load %s: %s\n", capability->dylib, dlerror());
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
capability->loaded = 1;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
static void load_capabilities(struct Load *load) {
|
|
161
|
+
char *config = malloc(MAX_CONFIG_BYTES);
|
|
162
|
+
if (config == NULL) return;
|
|
163
|
+
int status = read_config(load->config_path, config, MAX_CONFIG_BYTES);
|
|
164
|
+
if (status != 0) {
|
|
165
|
+
if (status > 0) {
|
|
166
|
+
fprintf(stderr, "[serve-sim] %s exceeds %d bytes; no capabilities loaded.\n",
|
|
167
|
+
load->config_path, MAX_CONFIG_BYTES);
|
|
168
|
+
}
|
|
169
|
+
config[0] = '\0';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
struct CapabilityLoad desired[MAX_CAPABILITIES];
|
|
173
|
+
size_t count = 0;
|
|
174
|
+
char *line, *lines = config;
|
|
175
|
+
while ((line = strsep(&lines, "\n")) != NULL) {
|
|
176
|
+
char *dylib, *env;
|
|
177
|
+
unsigned delay_ms;
|
|
178
|
+
if (!capability_applies(line, load->exec_path, &dylib, &env, &delay_ms)) continue;
|
|
179
|
+
if (count == MAX_CAPABILITIES) {
|
|
180
|
+
fprintf(stderr, "[serve-sim] more than %d capabilities apply; ignoring the rest.\n",
|
|
181
|
+
MAX_CAPABILITIES);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
desired[count++] = (struct CapabilityLoad){
|
|
185
|
+
.dylib = dylib, .env = env ? env : "", .delay_ms = delay_ms,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
for (size_t i = 0; i < MAX_CAPABILITIES; i++) {
|
|
189
|
+
struct CapabilityLoad *capability = &load->capabilities[i];
|
|
190
|
+
if (capability->dylib == NULL || capability->loaded) continue;
|
|
191
|
+
size_t j = 0;
|
|
192
|
+
while (j < count && strcmp(capability->dylib, desired[j].dylib) != 0) j++;
|
|
193
|
+
if (j == count) {
|
|
194
|
+
free(capability->dylib);
|
|
195
|
+
free(capability->env);
|
|
196
|
+
*capability = (struct CapabilityLoad){0};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (size_t entry = 0; entry < count; entry++) {
|
|
200
|
+
char *dylib = desired[entry].dylib;
|
|
201
|
+
char *env = desired[entry].env;
|
|
202
|
+
unsigned delay_ms = desired[entry].delay_ms;
|
|
203
|
+
struct CapabilityLoad *capability = NULL;
|
|
204
|
+
for (size_t i = 0; i < MAX_CAPABILITIES; i++) {
|
|
205
|
+
if (load->capabilities[i].dylib && strcmp(load->capabilities[i].dylib, dylib) == 0) {
|
|
206
|
+
capability = &load->capabilities[i];
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (capability && (capability->loaded ||
|
|
211
|
+
(capability->pending && capability->delay_ms == delay_ms && strcmp(capability->env, env) == 0))) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (capability == NULL) {
|
|
215
|
+
for (size_t i = 0; i < MAX_CAPABILITIES; i++) {
|
|
216
|
+
if (load->capabilities[i].dylib == NULL) {
|
|
217
|
+
capability = &load->capabilities[i];
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (capability == NULL) {
|
|
223
|
+
fprintf(stderr, "[serve-sim] capability load limit reached; ignoring %s\n", dylib);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
char *dylib_copy = strdup(dylib);
|
|
227
|
+
char *env_copy = strdup(env);
|
|
228
|
+
if (dylib_copy == NULL || env_copy == NULL) {
|
|
229
|
+
free(dylib_copy);
|
|
230
|
+
free(env_copy);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
free(capability->dylib);
|
|
234
|
+
free(capability->env);
|
|
235
|
+
*capability = (struct CapabilityLoad){
|
|
236
|
+
.dylib = dylib_copy, .env = env_copy, .delay_ms = delay_ms,
|
|
237
|
+
.generation = ++load->next_generation,
|
|
238
|
+
};
|
|
239
|
+
if (delay_ms == 0) {
|
|
240
|
+
load_one(capability);
|
|
241
|
+
} else {
|
|
242
|
+
capability->pending = 1;
|
|
243
|
+
uint64_t generation = capability->generation;
|
|
244
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)delay_ms * (int64_t)NSEC_PER_MSEC),
|
|
245
|
+
dispatch_get_main_queue(), ^{
|
|
246
|
+
load_capabilities(load);
|
|
247
|
+
if (capability->generation == generation && capability->dylib && !capability->loaded)
|
|
248
|
+
load_one(capability);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
free(config);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
static int config_dir(const char *path, char *out, size_t cap) {
|
|
256
|
+
int n = snprintf(out, cap, "%s", path);
|
|
257
|
+
if (n < 0 || (size_t)n >= cap) return -1;
|
|
258
|
+
char *slash = strrchr(out, '/');
|
|
259
|
+
if (slash == NULL || slash == out) return -1;
|
|
260
|
+
*slash = '\0';
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Watch the directory because config updates replace the file by rename.
|
|
265
|
+
static void watch_config(struct Load *load) {
|
|
266
|
+
char dir[sizeof load->config_path];
|
|
267
|
+
if (config_dir(load->config_path, dir, sizeof dir) != 0) return;
|
|
268
|
+
|
|
269
|
+
int fd = open(dir, O_EVTONLY);
|
|
270
|
+
if (fd < 0) return;
|
|
271
|
+
|
|
272
|
+
dispatch_source_t source = dispatch_source_create(
|
|
273
|
+
DISPATCH_SOURCE_TYPE_VNODE, (uintptr_t)fd, DISPATCH_VNODE_WRITE, dispatch_get_main_queue());
|
|
274
|
+
if (source == NULL) {
|
|
275
|
+
close(fd);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
dispatch_source_set_event_handler(source, ^{ load_capabilities(load); });
|
|
279
|
+
dispatch_source_set_cancel_handler(source, ^{ close(fd); });
|
|
280
|
+
dispatch_resume(source);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// getenv and access are safe in a constructor; the dlopen is not.
|
|
284
|
+
__attribute__((constructor))
|
|
285
|
+
static void serve_sim_capability_loader_init(void) {
|
|
286
|
+
const char *tmp = getenv("TMPDIR");
|
|
287
|
+
if (tmp == NULL || strstr(tmp, "/Containers/Data/Application/") == NULL) return;
|
|
288
|
+
|
|
289
|
+
// Absolute only. A relative path would resolve against the app's working
|
|
290
|
+
// directory, which is not ours to guess.
|
|
291
|
+
const char *config = getenv(CONFIG_VAR);
|
|
292
|
+
if (config == NULL || *config != '/') return;
|
|
293
|
+
|
|
294
|
+
struct Load *load = calloc(1, sizeof *load);
|
|
295
|
+
if (load == NULL) return;
|
|
296
|
+
char dir[sizeof load->config_path];
|
|
297
|
+
|
|
298
|
+
int n = snprintf(load->config_path, sizeof load->config_path, "%s", config);
|
|
299
|
+
if (n < 0 || (size_t)n >= sizeof load->config_path) {
|
|
300
|
+
free(load);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// The file need not exist yet: a capability turned on later creates it, and
|
|
304
|
+
// the watch is on the directory. Requiring the file here would mean an app
|
|
305
|
+
// started before the first capability could never receive one.
|
|
306
|
+
if (config_dir(load->config_path, dir, sizeof dir) != 0 || access(dir, R_OK | X_OK) != 0) {
|
|
307
|
+
free(load);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
uint32_t exec_size = sizeof load->exec_path;
|
|
311
|
+
if (_NSGetExecutablePath(load->exec_path, &exec_size) != 0) {
|
|
312
|
+
free(load);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Concurrent dlopen during construction can invert the dyld/ObjC load locks.
|
|
317
|
+
// Defer capability loads to the main queue after construction completes.
|
|
318
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
319
|
+
watch_config(load);
|
|
320
|
+
load_capabilities(load);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
Binary file
|