@stim-cli/server 1.19.0 → 1.20.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 +24 -7
- package/dist/{protocol-qo5TTavl.mjs → protocol-CjUChTWJ.mjs} +94 -7
- package/dist/protocol.mjs +1 -1
- package/dist/protocol.schema.json +139 -6
- package/dist/stim-frames/EmulatorFrameArtwork.swift +9 -0
- package/dist/stim-frames/SimulatorFrameArtwork.swift +9 -0
- package/dist/{stim-server-DThEHaj6.mjs → stim-server-C5iKxjba.mjs} +197 -53
- package/dist/stim-server.mjs +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -37,7 +37,8 @@ runs (see [Tailscale](#tailscale)). Stim Desktop starts its own server with the
|
|
|
37
37
|
unless it serves phones.
|
|
38
38
|
|
|
39
39
|
`--env KEY=VALUE` and `--path-prepend <dir>` (each repeatable) apply to the
|
|
40
|
-
serving command: see [Run as a service](#run-as-a-service).
|
|
40
|
+
serving command: see [Run as a service](#run-as-a-service). When serving, a bare
|
|
41
|
+
`--env KEY` takes the value from the server's own environment.
|
|
41
42
|
|
|
42
43
|
`pair --json` prints `{ "qr": <payload>, "expiresAt": "<ISO time>" }`, and
|
|
43
44
|
`devices --json` prints `{ "devices": [...] }` with each device's `id`, `name`,
|
|
@@ -280,6 +281,10 @@ loopback port: a forwarder that omits that header, such as `tailscale serve
|
|
|
280
281
|
|
|
281
282
|
A connection must send `hello` within 5 seconds. Five failed attempts from the
|
|
282
283
|
same peer within a minute block new connections from it for up to a minute.
|
|
284
|
+
The server answers 403 to a WebSocket upgrade that carries `Sec-Fetch-Site`, or
|
|
285
|
+
an `Origin` other than the requested host (`http:` on loopback, `https:`
|
|
286
|
+
through the route), and counts no failed attempt, so a web page can neither
|
|
287
|
+
connect nor block this Mac's clients.
|
|
283
288
|
|
|
284
289
|
Paired devices live in `$STIM_HOME/server/devices.json`. Revoking a device
|
|
285
290
|
closes its open connections. The server checks registrations on file changes
|
|
@@ -1048,9 +1053,15 @@ created a route to another `--port`, run `uninstall` first.
|
|
|
1048
1053
|
the server. stim-server replaces its environment with the login shell's at
|
|
1049
1054
|
start, so a `PATH` or `GEM_HOME` in the plist alone is lost and an entry in
|
|
1050
1055
|
`~/.zshrc` changes the terminal too. These flags apply after that capture, to
|
|
1051
|
-
the server and to the builds it starts
|
|
1052
|
-
`
|
|
1053
|
-
|
|
1056
|
+
the server and to the builds it starts. The plist stores each `--env` value in
|
|
1057
|
+
`EnvironmentVariables`, so it stays out of the server's process arguments, and
|
|
1058
|
+
passes only the name as `--env KEY`; the server reads that variable from the
|
|
1059
|
+
environment launchd gives it. The values sit in plain text in the plist. A
|
|
1060
|
+
plist written by an earlier release keeps `--env KEY=VALUE` in its
|
|
1061
|
+
`ProgramArguments` until the next `service install`. `--path-prepend` puts
|
|
1062
|
+
directories in front of PATH in the order given, as `ProgramArguments`; `--env`
|
|
1063
|
+
cannot set `STIM_HOME` or `SHELL`, which the job takes from the installing
|
|
1064
|
+
process. For a private CocoaPods:
|
|
1054
1065
|
|
|
1055
1066
|
```bash
|
|
1056
1067
|
stim-server service install --serve \
|
|
@@ -1119,7 +1130,9 @@ methods need that approval, not `read`:
|
|
|
1119
1130
|
it does not run as a `stim-server service` and so cannot update itself),
|
|
1120
1131
|
`acceptsClientBuilds`, `running` (the update in progress, with its `state`,
|
|
1121
1132
|
`missing` package bytes and the last lines of its `log`) and `last` (how the
|
|
1122
|
-
label's last `service update` ended, whoever ran it
|
|
1133
|
+
label's last `service update` ended, whoever ran it, or how the update this
|
|
1134
|
+
server started ended when that is newer, such as one the update lock refused,
|
|
1135
|
+
with the last line of its log).
|
|
1123
1136
|
- `server.update.start` takes `{ "release": "<version>" }`, an exact version
|
|
1124
1137
|
that `stim-server service update --release` installs, or
|
|
1125
1138
|
`{ "packages": [{ "name", "size", "sha256" }] }`, at most 8 `.tgz` packages
|
|
@@ -1189,7 +1202,11 @@ closes its connections and cancels its builds.
|
|
|
1189
1202
|
while the worker root's volume has less than 10 GiB free, while `builds`
|
|
1190
1203
|
reaches a non-zero `maxBuilds`, or while `loadPerCore` is at or above
|
|
1191
1204
|
`maxLoadPerCore`. The toolchain is read at most once a minute per Ruby context; capacity on
|
|
1192
|
-
every offer.
|
|
1205
|
+
every offer. With the `native-xcode-toolchain` feature, an offer can set
|
|
1206
|
+
`native: "xcode"` to collect only Stim, CPU, Xcode, iPhoneSimulator SDK and
|
|
1207
|
+
simulator-runtime facts. Unused report fields are null. This report has its own
|
|
1208
|
+
cache, ignores Ruby selection, and is reused for native Xcode build admission.
|
|
1209
|
+
Clients use the complete report on older native-build-capable hosts.
|
|
1193
1210
|
- `build.sync` takes `repo`, `files` and `done`. `files` is one page of the
|
|
1194
1211
|
manifest, each `{ "path", "kind": "file"|"exec"|"link", "size", "sha256" }`,
|
|
1195
1212
|
a link's blob being its target. Pages accumulate until `done`; the next
|
|
@@ -1329,7 +1346,7 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
1329
1346
|
(none without `control`), the paired device, and the new `deviceToken` when
|
|
1330
1347
|
the hello paired. `server.home` is the home folder
|
|
1331
1348
|
of the user the server runs as, so clients can show paths under it as
|
|
1332
|
-
`~/...`.
|
|
1349
|
+
`~/...`. A pending approval result omits it.
|
|
1333
1350
|
- `status.subscribe` returns a subscription id. Each `status` event carries a
|
|
1334
1351
|
full payload as `stim status --watch --json` prints it, including each
|
|
1335
1352
|
environment's `physicalDevices`, the phones it leases, and its `agents`, the
|
|
@@ -120,7 +120,11 @@ function protocolJsonSchema() {
|
|
|
120
120
|
session: { type: "string" },
|
|
121
121
|
attempt: { type: "string" },
|
|
122
122
|
bundleId: { type: "string" },
|
|
123
|
-
mode: { enum: [
|
|
123
|
+
mode: { enum: [
|
|
124
|
+
"development",
|
|
125
|
+
"release",
|
|
126
|
+
"process"
|
|
127
|
+
] },
|
|
124
128
|
devClientScheme: {
|
|
125
129
|
type: "string",
|
|
126
130
|
pattern: "^[a-zA-Z][a-zA-Z0-9+.-]{0,127}$"
|
|
@@ -864,8 +868,7 @@ function protocolJsonSchema() {
|
|
|
864
868
|
required: [
|
|
865
869
|
"name",
|
|
866
870
|
"version",
|
|
867
|
-
"stim"
|
|
868
|
-
"home"
|
|
871
|
+
"stim"
|
|
869
872
|
],
|
|
870
873
|
additionalProperties: false,
|
|
871
874
|
properties: {
|
|
@@ -1706,7 +1709,11 @@ function protocolJsonSchema() {
|
|
|
1706
1709
|
pattern: "^[a-zA-Z0-9_-]{1,128}$"
|
|
1707
1710
|
},
|
|
1708
1711
|
bundleId: { type: "string" },
|
|
1709
|
-
mode: { enum: [
|
|
1712
|
+
mode: { enum: [
|
|
1713
|
+
"development",
|
|
1714
|
+
"release",
|
|
1715
|
+
"process"
|
|
1716
|
+
] },
|
|
1710
1717
|
devClientScheme: {
|
|
1711
1718
|
type: "string",
|
|
1712
1719
|
pattern: "^[a-zA-Z][a-zA-Z0-9+.-]{0,127}$"
|
|
@@ -2217,7 +2224,8 @@ function protocolJsonSchema() {
|
|
|
2217
2224
|
rubyVersion: {
|
|
2218
2225
|
type: "string",
|
|
2219
2226
|
pattern: BUILD_RUBY_VERSION_PATTERN
|
|
2220
|
-
}
|
|
2227
|
+
},
|
|
2228
|
+
native: { enum: ["xcode", "gradle"] }
|
|
2221
2229
|
}
|
|
2222
2230
|
}),
|
|
2223
2231
|
request("build.sync", {
|
|
@@ -2249,7 +2257,8 @@ function protocolJsonSchema() {
|
|
|
2249
2257
|
kind: { enum: [
|
|
2250
2258
|
"file",
|
|
2251
2259
|
"exec",
|
|
2252
|
-
"link"
|
|
2260
|
+
"link",
|
|
2261
|
+
"directory"
|
|
2253
2262
|
] },
|
|
2254
2263
|
size: {
|
|
2255
2264
|
type: "integer",
|
|
@@ -2264,6 +2273,18 @@ function protocolJsonSchema() {
|
|
|
2264
2273
|
}),
|
|
2265
2274
|
request("build.start", {
|
|
2266
2275
|
type: "object",
|
|
2276
|
+
if: {
|
|
2277
|
+
required: ["native"],
|
|
2278
|
+
properties: { native: { properties: { provider: { const: "gradle" } } } }
|
|
2279
|
+
},
|
|
2280
|
+
then: { properties: {
|
|
2281
|
+
platform: { const: "android" },
|
|
2282
|
+
fingerprint: { type: "null" }
|
|
2283
|
+
} },
|
|
2284
|
+
else: { properties: { fingerprint: {
|
|
2285
|
+
type: "string",
|
|
2286
|
+
minLength: 1
|
|
2287
|
+
} } },
|
|
2267
2288
|
required: [
|
|
2268
2289
|
"repo",
|
|
2269
2290
|
"project",
|
|
@@ -2307,9 +2328,75 @@ function protocolJsonSchema() {
|
|
|
2307
2328
|
minLength: 1
|
|
2308
2329
|
},
|
|
2309
2330
|
fingerprint: {
|
|
2310
|
-
type: "string",
|
|
2331
|
+
type: ["string", "null"],
|
|
2311
2332
|
minLength: 1
|
|
2312
2333
|
},
|
|
2334
|
+
native: { oneOf: [{
|
|
2335
|
+
type: "object",
|
|
2336
|
+
required: [
|
|
2337
|
+
"provider",
|
|
2338
|
+
"sourceDigest",
|
|
2339
|
+
"cacheKey",
|
|
2340
|
+
"arch",
|
|
2341
|
+
"parameters"
|
|
2342
|
+
],
|
|
2343
|
+
additionalProperties: false,
|
|
2344
|
+
properties: {
|
|
2345
|
+
provider: { const: "xcode" },
|
|
2346
|
+
sourceDigest: sha256,
|
|
2347
|
+
cacheKey: {
|
|
2348
|
+
type: "string",
|
|
2349
|
+
minLength: 1
|
|
2350
|
+
},
|
|
2351
|
+
arch: { enum: [
|
|
2352
|
+
"arm64",
|
|
2353
|
+
"x86_64",
|
|
2354
|
+
null
|
|
2355
|
+
] },
|
|
2356
|
+
parameters: {
|
|
2357
|
+
type: "object",
|
|
2358
|
+
description: "The non-file parameters of the client artifact identity, to name what differs."
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
}, {
|
|
2362
|
+
type: "object",
|
|
2363
|
+
required: [
|
|
2364
|
+
"provider",
|
|
2365
|
+
"sourceDigest",
|
|
2366
|
+
"inputs"
|
|
2367
|
+
],
|
|
2368
|
+
additionalProperties: false,
|
|
2369
|
+
properties: {
|
|
2370
|
+
provider: { const: "gradle" },
|
|
2371
|
+
sourceDigest: sha256,
|
|
2372
|
+
inputs: {
|
|
2373
|
+
type: "object",
|
|
2374
|
+
required: [
|
|
2375
|
+
"complete",
|
|
2376
|
+
"ignored",
|
|
2377
|
+
"outputs"
|
|
2378
|
+
],
|
|
2379
|
+
additionalProperties: false,
|
|
2380
|
+
properties: {
|
|
2381
|
+
complete: { const: true },
|
|
2382
|
+
ignored: {
|
|
2383
|
+
type: "array",
|
|
2384
|
+
items: {
|
|
2385
|
+
type: "string",
|
|
2386
|
+
minLength: 1
|
|
2387
|
+
}
|
|
2388
|
+
},
|
|
2389
|
+
outputs: {
|
|
2390
|
+
type: "array",
|
|
2391
|
+
items: {
|
|
2392
|
+
type: "string",
|
|
2393
|
+
minLength: 1
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}] },
|
|
2313
2400
|
packageName: { type: ["string", "null"] },
|
|
2314
2401
|
isExpo: { type: "boolean" },
|
|
2315
2402
|
optimizations: { type: ["object", "null"] },
|
package/dist/protocol.mjs
CHANGED
|
@@ -70,7 +70,8 @@
|
|
|
70
70
|
"mode": {
|
|
71
71
|
"enum": [
|
|
72
72
|
"development",
|
|
73
|
-
"release"
|
|
73
|
+
"release",
|
|
74
|
+
"process"
|
|
74
75
|
]
|
|
75
76
|
},
|
|
76
77
|
"devClientScheme": {
|
|
@@ -1260,8 +1261,7 @@
|
|
|
1260
1261
|
"required": [
|
|
1261
1262
|
"name",
|
|
1262
1263
|
"version",
|
|
1263
|
-
"stim"
|
|
1264
|
-
"home"
|
|
1264
|
+
"stim"
|
|
1265
1265
|
],
|
|
1266
1266
|
"additionalProperties": false,
|
|
1267
1267
|
"properties": {
|
|
@@ -1312,7 +1312,12 @@
|
|
|
1312
1312
|
"workspace-diff",
|
|
1313
1313
|
"hosted-congestion",
|
|
1314
1314
|
"hosted-ios-data",
|
|
1315
|
+
"hosted-ios-process",
|
|
1316
|
+
"native-xcode-build",
|
|
1317
|
+
"native-xcode-toolchain",
|
|
1318
|
+
"native-gradle-build",
|
|
1315
1319
|
"hosted-android-data",
|
|
1320
|
+
"hosted-android-process",
|
|
1316
1321
|
"server-update"
|
|
1317
1322
|
]
|
|
1318
1323
|
}
|
|
@@ -2778,7 +2783,8 @@
|
|
|
2778
2783
|
"mode": {
|
|
2779
2784
|
"enum": [
|
|
2780
2785
|
"development",
|
|
2781
|
-
"release"
|
|
2786
|
+
"release",
|
|
2787
|
+
"process"
|
|
2782
2788
|
]
|
|
2783
2789
|
},
|
|
2784
2790
|
"devClientScheme": {
|
|
@@ -5224,6 +5230,12 @@
|
|
|
5224
5230
|
"rubyVersion": {
|
|
5225
5231
|
"type": "string",
|
|
5226
5232
|
"pattern": "^(?!\\.{1,2}$)[^/\\\\\\u0000\\r\\n]+$"
|
|
5233
|
+
},
|
|
5234
|
+
"native": {
|
|
5235
|
+
"enum": [
|
|
5236
|
+
"xcode",
|
|
5237
|
+
"gradle"
|
|
5238
|
+
]
|
|
5227
5239
|
}
|
|
5228
5240
|
}
|
|
5229
5241
|
}
|
|
@@ -5280,7 +5292,8 @@
|
|
|
5280
5292
|
"enum": [
|
|
5281
5293
|
"file",
|
|
5282
5294
|
"exec",
|
|
5283
|
-
"link"
|
|
5295
|
+
"link",
|
|
5296
|
+
"directory"
|
|
5284
5297
|
]
|
|
5285
5298
|
},
|
|
5286
5299
|
"size": {
|
|
@@ -5321,6 +5334,38 @@
|
|
|
5321
5334
|
},
|
|
5322
5335
|
"params": {
|
|
5323
5336
|
"type": "object",
|
|
5337
|
+
"if": {
|
|
5338
|
+
"required": [
|
|
5339
|
+
"native"
|
|
5340
|
+
],
|
|
5341
|
+
"properties": {
|
|
5342
|
+
"native": {
|
|
5343
|
+
"properties": {
|
|
5344
|
+
"provider": {
|
|
5345
|
+
"const": "gradle"
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
},
|
|
5351
|
+
"then": {
|
|
5352
|
+
"properties": {
|
|
5353
|
+
"platform": {
|
|
5354
|
+
"const": "android"
|
|
5355
|
+
},
|
|
5356
|
+
"fingerprint": {
|
|
5357
|
+
"type": "null"
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5360
|
+
},
|
|
5361
|
+
"else": {
|
|
5362
|
+
"properties": {
|
|
5363
|
+
"fingerprint": {
|
|
5364
|
+
"type": "string",
|
|
5365
|
+
"minLength": 1
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
},
|
|
5324
5369
|
"required": [
|
|
5325
5370
|
"repo",
|
|
5326
5371
|
"project",
|
|
@@ -5398,9 +5443,97 @@
|
|
|
5398
5443
|
"minLength": 1
|
|
5399
5444
|
},
|
|
5400
5445
|
"fingerprint": {
|
|
5401
|
-
"type":
|
|
5446
|
+
"type": [
|
|
5447
|
+
"string",
|
|
5448
|
+
"null"
|
|
5449
|
+
],
|
|
5402
5450
|
"minLength": 1
|
|
5403
5451
|
},
|
|
5452
|
+
"native": {
|
|
5453
|
+
"oneOf": [
|
|
5454
|
+
{
|
|
5455
|
+
"type": "object",
|
|
5456
|
+
"required": [
|
|
5457
|
+
"provider",
|
|
5458
|
+
"sourceDigest",
|
|
5459
|
+
"cacheKey",
|
|
5460
|
+
"arch",
|
|
5461
|
+
"parameters"
|
|
5462
|
+
],
|
|
5463
|
+
"additionalProperties": false,
|
|
5464
|
+
"properties": {
|
|
5465
|
+
"provider": {
|
|
5466
|
+
"const": "xcode"
|
|
5467
|
+
},
|
|
5468
|
+
"sourceDigest": {
|
|
5469
|
+
"type": "string",
|
|
5470
|
+
"pattern": "^[0-9a-f]{64}$"
|
|
5471
|
+
},
|
|
5472
|
+
"cacheKey": {
|
|
5473
|
+
"type": "string",
|
|
5474
|
+
"minLength": 1
|
|
5475
|
+
},
|
|
5476
|
+
"arch": {
|
|
5477
|
+
"enum": [
|
|
5478
|
+
"arm64",
|
|
5479
|
+
"x86_64",
|
|
5480
|
+
null
|
|
5481
|
+
]
|
|
5482
|
+
},
|
|
5483
|
+
"parameters": {
|
|
5484
|
+
"type": "object",
|
|
5485
|
+
"description": "The non-file parameters of the client artifact identity, to name what differs."
|
|
5486
|
+
}
|
|
5487
|
+
}
|
|
5488
|
+
},
|
|
5489
|
+
{
|
|
5490
|
+
"type": "object",
|
|
5491
|
+
"required": [
|
|
5492
|
+
"provider",
|
|
5493
|
+
"sourceDigest",
|
|
5494
|
+
"inputs"
|
|
5495
|
+
],
|
|
5496
|
+
"additionalProperties": false,
|
|
5497
|
+
"properties": {
|
|
5498
|
+
"provider": {
|
|
5499
|
+
"const": "gradle"
|
|
5500
|
+
},
|
|
5501
|
+
"sourceDigest": {
|
|
5502
|
+
"type": "string",
|
|
5503
|
+
"pattern": "^[0-9a-f]{64}$"
|
|
5504
|
+
},
|
|
5505
|
+
"inputs": {
|
|
5506
|
+
"type": "object",
|
|
5507
|
+
"required": [
|
|
5508
|
+
"complete",
|
|
5509
|
+
"ignored",
|
|
5510
|
+
"outputs"
|
|
5511
|
+
],
|
|
5512
|
+
"additionalProperties": false,
|
|
5513
|
+
"properties": {
|
|
5514
|
+
"complete": {
|
|
5515
|
+
"const": true
|
|
5516
|
+
},
|
|
5517
|
+
"ignored": {
|
|
5518
|
+
"type": "array",
|
|
5519
|
+
"items": {
|
|
5520
|
+
"type": "string",
|
|
5521
|
+
"minLength": 1
|
|
5522
|
+
}
|
|
5523
|
+
},
|
|
5524
|
+
"outputs": {
|
|
5525
|
+
"type": "array",
|
|
5526
|
+
"items": {
|
|
5527
|
+
"type": "string",
|
|
5528
|
+
"minLength": 1
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5533
|
+
}
|
|
5534
|
+
}
|
|
5535
|
+
]
|
|
5536
|
+
},
|
|
5404
5537
|
"packageName": {
|
|
5405
5538
|
"type": [
|
|
5406
5539
|
"string",
|
|
@@ -66,6 +66,15 @@ private struct SkinNode {
|
|
|
66
66
|
|
|
67
67
|
@MainActor
|
|
68
68
|
enum EmulatorFrameArtwork {
|
|
69
|
+
private static var cache: [String: DeviceFrameArtwork] = [:]
|
|
70
|
+
|
|
71
|
+
static func cached(avdName: String) -> DeviceFrameArtwork? {
|
|
72
|
+
if let hit = cache[avdName] { return hit }
|
|
73
|
+
let artwork = load(avdName: avdName)
|
|
74
|
+
if let artwork { cache[avdName] = artwork }
|
|
75
|
+
return artwork
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
static func load(avdName: String) -> DeviceFrameArtwork? {
|
|
70
79
|
let environment = ProcessInfo.processInfo.environment
|
|
71
80
|
let home = FileManager.default.homeDirectoryForCurrentUser
|
|
@@ -7,6 +7,15 @@ import AppKit
|
|
|
7
7
|
|
|
8
8
|
@MainActor
|
|
9
9
|
enum SimulatorFrameArtwork {
|
|
10
|
+
private static var cache: [String: DeviceFrameArtwork] = [:]
|
|
11
|
+
|
|
12
|
+
static func cached(udid: String) -> DeviceFrameArtwork? {
|
|
13
|
+
if let hit = cache[udid] { return hit }
|
|
14
|
+
let artwork = load(udid: udid)
|
|
15
|
+
if let artwork { cache[udid] = artwork }
|
|
16
|
+
return artwork
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
static func load(udid: String) -> DeviceFrameArtwork? {
|
|
11
20
|
guard let resources = resources(udid: udid) else { return nil }
|
|
12
21
|
let profile = plist(resources.appendingPathComponent("profile.plist"))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as protocol_exports } from "./protocol-
|
|
1
|
+
import { n as protocol_exports } from "./protocol-CjUChTWJ.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -187,6 +187,13 @@ const MAX_CLIENT_NAME = 64;
|
|
|
187
187
|
function validClientName(name) {
|
|
188
188
|
return name.length <= MAX_CLIENT_NAME && !/[\p{Cc}\p{Cf}]/u.test(name);
|
|
189
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* A phone sends its OS device name, which the person cannot edit in the app, so pairing cleans it up instead of
|
|
192
|
+
* refusing it. U+200D stays because emoji sequences join with it.
|
|
193
|
+
*/
|
|
194
|
+
function pairingName(name) {
|
|
195
|
+
return name.replace(/(?![\u200d\s])[\p{Cc}\p{Cf}]/gu, "").replace(/\s+/g, " ").trim().slice(0, MAX_CLIENT_NAME).replace(/[\ud800-\udbff]$/, "").trim() || "Phone";
|
|
196
|
+
}
|
|
190
197
|
function newDeviceId() {
|
|
191
198
|
const taken = new Set([
|
|
192
199
|
...readDevices(),
|
|
@@ -331,7 +338,7 @@ function spendPairingToken(token, name, identity, now = Date.now()) {
|
|
|
331
338
|
const at = new Date(now).toISOString();
|
|
332
339
|
const device = {
|
|
333
340
|
id: newDeviceId(),
|
|
334
|
-
name,
|
|
341
|
+
name: pairingName(name),
|
|
335
342
|
tokenHash: hashToken(deviceToken),
|
|
336
343
|
identity,
|
|
337
344
|
pairedAt: at,
|
|
@@ -659,6 +666,7 @@ const DEFAULT_PORT = 7787;
|
|
|
659
666
|
const THROTTLE_SECONDS = 30;
|
|
660
667
|
const LABEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
661
668
|
const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
669
|
+
const JOB_KEYS = ["STIM_HOME", "SHELL"];
|
|
662
670
|
function validateLabel(label) {
|
|
663
671
|
return LABEL_PATTERN.test(label) ? null : `--label must be letters, digits, ".", "_" or "-", got "${label}".`;
|
|
664
672
|
}
|
|
@@ -679,13 +687,17 @@ const illegal = (value) => [...value].some((char) => {
|
|
|
679
687
|
const code = char.codePointAt(0);
|
|
680
688
|
return code < 32 && code !== 9 && code !== 10 && code !== 13;
|
|
681
689
|
});
|
|
682
|
-
/**
|
|
683
|
-
|
|
690
|
+
/**
|
|
691
|
+
* Returns the first problem in the `--env` and `--path-prepend` values, or null when both are usable. `serving` also
|
|
692
|
+
* accepts a bare `KEY`, which the serving command reads from its own environment, and tolerates `STIM_HOME` and
|
|
693
|
+
* `SHELL`, which a plist written by an earlier release can carry and `applyServeEnvironment` ignores.
|
|
694
|
+
*/
|
|
695
|
+
function validateServeEnvironment(env, pathPrepend, serving = false) {
|
|
684
696
|
if ([...env, ...pathPrepend].some(illegal)) return "--env and --path-prepend values cannot contain control characters.";
|
|
685
697
|
for (const entry of env) {
|
|
686
|
-
const parsed = parseEnvAssignment(entry);
|
|
698
|
+
const parsed = serving && ENV_KEY_PATTERN.test(entry) ? { key: entry } : parseEnvAssignment(entry);
|
|
687
699
|
if (typeof parsed === "string") return parsed;
|
|
688
|
-
if (parsed.key
|
|
700
|
+
if (!serving && JOB_KEYS.includes(parsed.key)) return `--env cannot set ${parsed.key}; start stim-server with ${parsed.key} set instead.`;
|
|
689
701
|
}
|
|
690
702
|
for (const dir of pathPrepend) if (!isAbsolute(dir) || dir.includes(delimiter)) return `--path-prepend takes one absolute directory, got "${dir}".`;
|
|
691
703
|
return null;
|
|
@@ -693,22 +705,43 @@ function validateServeEnvironment(env, pathPrepend) {
|
|
|
693
705
|
/**
|
|
694
706
|
* The environment stim-server runs with: the login shell's, then each `--env` entry, then the `--path-prepend`
|
|
695
707
|
* directories in front of `PATH`, first directory first. Both flags apply after the login shell's environment
|
|
696
|
-
* replaces the process environment, so they survive it.
|
|
708
|
+
* replaces the process environment, so they survive it. A bare `KEY` entry takes its value from `source`, the
|
|
709
|
+
* environment launchd gave the process; one that `source` lacks is skipped with a line on stderr. `STIM_HOME` and
|
|
710
|
+
* `SHELL` entries are skipped the same way: the job takes them from the installing process.
|
|
697
711
|
*/
|
|
698
|
-
function applyServeEnvironment(base, env, pathPrepend) {
|
|
712
|
+
function applyServeEnvironment(base, env, pathPrepend, source = process.env) {
|
|
699
713
|
const result = { ...base };
|
|
700
714
|
for (const entry of env) {
|
|
701
715
|
const parsed = parseEnvAssignment(entry);
|
|
702
|
-
|
|
716
|
+
const name = typeof parsed === "string" ? entry : parsed.key;
|
|
717
|
+
if (JOB_KEYS.includes(name)) console.error(`stim-server: ignoring --env ${name}; the service takes it from the process that installed it.`);
|
|
718
|
+
else if (typeof parsed !== "string") result[name] = parsed.value;
|
|
719
|
+
else if (ENV_KEY_PATTERN.test(entry)) {
|
|
720
|
+
const value = source[entry];
|
|
721
|
+
if (value === void 0) console.error(`stim-server: --env ${entry} has no value in the service environment; skipping it.`);
|
|
722
|
+
else result[entry] = value;
|
|
723
|
+
}
|
|
703
724
|
}
|
|
704
725
|
if (pathPrepend.length) result.PATH = [...pathPrepend, ...result.PATH ? [result.PATH] : []].join(delimiter);
|
|
705
726
|
return result;
|
|
706
727
|
}
|
|
728
|
+
function environmentValues(env) {
|
|
729
|
+
const values = /* @__PURE__ */ new Map();
|
|
730
|
+
for (const entry of env) {
|
|
731
|
+
const parsed = parseEnvAssignment(entry);
|
|
732
|
+
if (typeof parsed !== "string" && !JOB_KEYS.includes(parsed.key)) values.set(parsed.key, parsed.value);
|
|
733
|
+
}
|
|
734
|
+
return values;
|
|
735
|
+
}
|
|
736
|
+
/** `env` as the plist stores it: one `KEY=VALUE` per key, the last value winning, without the keys the job supplies. */
|
|
737
|
+
function normalizeEnv(env) {
|
|
738
|
+
return [...environmentValues(env)].map(([name, value]) => `${name}=${value}`);
|
|
739
|
+
}
|
|
707
740
|
function serverArguments(spec) {
|
|
708
741
|
return [
|
|
709
742
|
"--port",
|
|
710
743
|
String(spec.port),
|
|
711
|
-
...spec.env.flatMap((
|
|
744
|
+
...[...environmentValues(spec.env).keys()].flatMap((name) => ["--env", name]),
|
|
712
745
|
...spec.pathPrepend.flatMap((dir) => ["--path-prepend", dir])
|
|
713
746
|
];
|
|
714
747
|
}
|
|
@@ -736,8 +769,9 @@ function renderPlist(spec) {
|
|
|
736
769
|
].map((argument) => string(argument, " ")),
|
|
737
770
|
" </array>"
|
|
738
771
|
];
|
|
739
|
-
const environment = Object.entries(spec.environment);
|
|
740
|
-
|
|
772
|
+
const environment = new Map(Object.entries(spec.environment));
|
|
773
|
+
for (const [name, value] of environmentValues(spec.env)) environment.set(name, value);
|
|
774
|
+
if (environment.size) {
|
|
741
775
|
lines.push(key("EnvironmentVariables", " "), " <dict>");
|
|
742
776
|
for (const [name, value] of environment) lines.push(key(name, " "), string(value, " "));
|
|
743
777
|
lines.push(" </dict>");
|
|
@@ -758,6 +792,7 @@ function parseInstalledPlist(value) {
|
|
|
758
792
|
const programArguments = strings(value.ProgramArguments);
|
|
759
793
|
const host = programArguments[1] === "run" && programArguments[0]?.endsWith("/Contents/MacOS/stim-host") ? programArguments[0] : null;
|
|
760
794
|
const [node = null, script = null, ...args] = host ? programArguments.slice(2) : programArguments;
|
|
795
|
+
const environment = isJsonObject(value.EnvironmentVariables) ? value.EnvironmentVariables : {};
|
|
761
796
|
const env = [];
|
|
762
797
|
const pathPrepend = [];
|
|
763
798
|
let port = null;
|
|
@@ -766,10 +801,13 @@ function parseInstalledPlist(value) {
|
|
|
766
801
|
const next = args[index + 1];
|
|
767
802
|
if (next === void 0) break;
|
|
768
803
|
if (flag === "--port") port = Number(next);
|
|
769
|
-
if (flag === "--env")
|
|
804
|
+
if (flag === "--env") {
|
|
805
|
+
const stored = environment[next];
|
|
806
|
+
if (next.includes("=")) env.push(next);
|
|
807
|
+
else if (typeof stored === "string") env.push(`${next}=${stored}`);
|
|
808
|
+
}
|
|
770
809
|
if (flag === "--path-prepend") pathPrepend.push(next);
|
|
771
810
|
}
|
|
772
|
-
const environment = isJsonObject(value.EnvironmentVariables) ? value.EnvironmentVariables : {};
|
|
773
811
|
const meta = isJsonObject(value.StimService) ? value.StimService : {};
|
|
774
812
|
const servePort = meta.ServePort;
|
|
775
813
|
return {
|
|
@@ -1582,7 +1620,7 @@ async function installJob(options, setup) {
|
|
|
1582
1620
|
};
|
|
1583
1621
|
const path = plistPath(options.label);
|
|
1584
1622
|
const tailscale = findTailscale(process.env) ?? "tailscale";
|
|
1585
|
-
if (setup && previous?.script === script && previous.port === options.port && previous.host === spec.host && JSON.stringify(previous.env) === JSON.stringify(spec.env) && JSON.stringify(previous.pathPrepend) === JSON.stringify(spec.pathPrepend)) {
|
|
1623
|
+
if (setup && previous?.script === script && previous.port === options.port && previous.host === spec.host && JSON.stringify(normalizeEnv(previous.env)) === JSON.stringify(normalizeEnv(spec.env)) && JSON.stringify(previous.pathPrepend) === JSON.stringify(spec.pathPrepend)) {
|
|
1586
1624
|
const health = await fetchHealth(options.port);
|
|
1587
1625
|
if (health && health.startup?.state !== "pending" && health.startup?.state !== "degraded") return [`${options.label} already runs stim-server ${health.version}.`];
|
|
1588
1626
|
}
|
|
@@ -2521,7 +2559,8 @@ function validBuildPath(path) {
|
|
|
2521
2559
|
}
|
|
2522
2560
|
function validFile(file) {
|
|
2523
2561
|
if (!isJsonObject(file) || !validBuildPath(file.path)) return false;
|
|
2524
|
-
if (file.kind !== "file" && file.kind !== "exec" && file.kind !== "link") return false;
|
|
2562
|
+
if (file.kind !== "file" && file.kind !== "exec" && file.kind !== "link" && file.kind !== "directory") return false;
|
|
2563
|
+
if (file.kind === "directory" && (file.size !== 0 || file.sha256 !== createHash("sha256").update("").digest("hex"))) return false;
|
|
2525
2564
|
const size = file.size;
|
|
2526
2565
|
if (typeof size !== "number" || !Number.isInteger(size) || size < 0 || size > MAX_FILE_BYTES) return false;
|
|
2527
2566
|
return typeof file.sha256 === "string" && /^[0-9a-f]{64}$/.test(file.sha256);
|
|
@@ -2530,21 +2569,27 @@ const validRepo = (repo) => typeof repo === "string" && new RegExp(protocol_expo
|
|
|
2530
2569
|
const gb = (bytes) => (bytes / 1024 ** 3).toFixed(1);
|
|
2531
2570
|
const optional = (value) => typeof value === "string" ? value : null;
|
|
2532
2571
|
const GRADLE_NAME = /^[A-Za-z0-9_-]{1,100}$/;
|
|
2572
|
+
const GRADLE_ROOT = /^[A-Za-z0-9._/ -]{1,200}$/;
|
|
2573
|
+
const GRADLE_MODULE = /^(:(?!\.\.?(?::|$))[A-Za-z0-9._-]+){1,20}$/;
|
|
2533
2574
|
/** The Gradle choices of an Android `build.start`, or null when they are malformed. */
|
|
2534
2575
|
function androidOptions(value) {
|
|
2535
2576
|
if (!isJsonObject(value)) return null;
|
|
2536
|
-
const { variant, abi, gradleBuildCache, pch, compilerCache } = value;
|
|
2577
|
+
const { variant, abi, gradleBuildCache, pch, compilerCache, gradleRoot = null, module = null } = value;
|
|
2537
2578
|
if (variant !== null && !(typeof variant === "string" && GRADLE_NAME.test(variant))) return null;
|
|
2538
2579
|
if (abi !== null && !(typeof abi === "string" && GRADLE_NAME.test(abi))) return null;
|
|
2539
2580
|
if (typeof gradleBuildCache !== "boolean") return null;
|
|
2540
2581
|
if (pch !== "auto" && pch !== "on" && pch !== "off") return null;
|
|
2541
2582
|
if (compilerCache !== "ccache" && compilerCache !== "none") return null;
|
|
2583
|
+
if (gradleRoot !== null && !(typeof gradleRoot === "string" && GRADLE_ROOT.test(gradleRoot))) return null;
|
|
2584
|
+
if (module !== null && !(typeof module === "string" && GRADLE_MODULE.test(module))) return null;
|
|
2542
2585
|
return {
|
|
2543
2586
|
variant,
|
|
2544
2587
|
abi,
|
|
2545
2588
|
gradleBuildCache,
|
|
2546
2589
|
pch,
|
|
2547
|
-
compilerCache
|
|
2590
|
+
compilerCache,
|
|
2591
|
+
gradleRoot,
|
|
2592
|
+
module
|
|
2548
2593
|
};
|
|
2549
2594
|
}
|
|
2550
2595
|
function freeBytes(path) {
|
|
@@ -2649,17 +2694,15 @@ var BuildHost = class {
|
|
|
2649
2694
|
clientDir(client) {
|
|
2650
2695
|
return join(this.root(), client);
|
|
2651
2696
|
}
|
|
2652
|
-
toolchain(rubyVersion = null) {
|
|
2697
|
+
toolchain(rubyVersion = null, native) {
|
|
2698
|
+
const key = JSON.stringify([native ?? null, native ? null : rubyVersion]);
|
|
2653
2699
|
const now = Date.now();
|
|
2654
2700
|
for (const [context, entry] of this.toolchains) if (now - entry.at >= this.limits.toolchainTtlMs) this.toolchains.delete(context);
|
|
2655
|
-
const cached = this.toolchains.get(
|
|
2701
|
+
const cached = this.toolchains.get(key);
|
|
2656
2702
|
if (cached) return cached.value;
|
|
2657
2703
|
const value = new Promise((resolve) => {
|
|
2658
|
-
const
|
|
2659
|
-
|
|
2660
|
-
"offer",
|
|
2661
|
-
...rubyVersion ? [rubyVersion] : []
|
|
2662
|
-
], {
|
|
2704
|
+
const args = native ? [`offer-native-${native}`] : ["offer", ...rubyVersion ? [rubyVersion] : []];
|
|
2705
|
+
const child = spawn(process.execPath, [this.options.worker, ...args], {
|
|
2663
2706
|
env: this.options.env,
|
|
2664
2707
|
stdio: [
|
|
2665
2708
|
"ignore",
|
|
@@ -2682,19 +2725,20 @@ var BuildHost = class {
|
|
|
2682
2725
|
}
|
|
2683
2726
|
});
|
|
2684
2727
|
});
|
|
2685
|
-
this.toolchains.set(
|
|
2728
|
+
this.toolchains.set(key, {
|
|
2686
2729
|
at: now,
|
|
2687
2730
|
value
|
|
2688
2731
|
});
|
|
2689
2732
|
value.then((result) => {
|
|
2690
|
-
if (!result && this.toolchains.get(
|
|
2733
|
+
if (!result && this.toolchains.get(key)?.value === value) this.toolchains.delete(key);
|
|
2691
2734
|
});
|
|
2692
2735
|
return value;
|
|
2693
2736
|
}
|
|
2694
2737
|
async offer(client, params) {
|
|
2695
2738
|
if (!isJsonObject(params) || !validRepo(params.repo)) return refusal("bad-request", "build.offer needs params.repo.");
|
|
2696
2739
|
if (params.rubyVersion !== void 0 && (typeof params.rubyVersion !== "string" || !new RegExp(protocol_exports.BUILD_RUBY_VERSION_PATTERN).test(params.rubyVersion))) return refusal("bad-request", "build.offer needs a Ruby installation name without path separators.");
|
|
2697
|
-
|
|
2740
|
+
if (params.native !== void 0 && params.native !== "xcode" && params.native !== "gradle") return refusal("bad-request", "build.offer supports native Xcode or Gradle toolchains.");
|
|
2741
|
+
const toolchain = await this.toolchain(params.rubyVersion ?? null, params.native);
|
|
2698
2742
|
if (!toolchain) return refusal("build-refused", "This Mac could not read its build toolchain.");
|
|
2699
2743
|
const root = this.root();
|
|
2700
2744
|
mkdirSync(root, {
|
|
@@ -2920,7 +2964,7 @@ var BuildHost = class {
|
|
|
2920
2964
|
job: id,
|
|
2921
2965
|
record: value.record
|
|
2922
2966
|
});
|
|
2923
|
-
else if (value.type === "result") entry.outcome = workerOutcome(value);
|
|
2967
|
+
else if (value.type === "result") entry.outcome = workerOutcome(value, isJsonObject(job.native) && job.native.provider === "gradle" ? "gradle" : void 0);
|
|
2924
2968
|
});
|
|
2925
2969
|
this.jobs.add(entry);
|
|
2926
2970
|
this.owned.set(id, entry);
|
|
@@ -3079,18 +3123,20 @@ var BuildHost = class {
|
|
|
3079
3123
|
await this.sweeping;
|
|
3080
3124
|
}
|
|
3081
3125
|
};
|
|
3082
|
-
function workerOutcome(value) {
|
|
3126
|
+
function workerOutcome(value, native) {
|
|
3083
3127
|
const timings = isJsonObject(value.timings) ? value.timings : {};
|
|
3084
3128
|
if (value.ok === true && isJsonObject(value.artifact)) {
|
|
3085
3129
|
const { name, size, sha256 } = value.artifact;
|
|
3086
|
-
if (typeof name === "string" && typeof size === "number" && typeof sha256 === "string") return {
|
|
3130
|
+
if (typeof name === "string" && typeof size === "number" && typeof sha256 === "string" && (native !== "gradle" || value.fingerprint === null && typeof value.androidPackage === "string" && /^[A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)+$/.test(value.androidPackage))) return {
|
|
3087
3131
|
ok: true,
|
|
3088
3132
|
artifact: {
|
|
3089
3133
|
name,
|
|
3090
3134
|
size,
|
|
3091
3135
|
sha256
|
|
3092
3136
|
},
|
|
3093
|
-
fingerprint: String(value.fingerprint),
|
|
3137
|
+
fingerprint: native === "gradle" ? null : String(value.fingerprint),
|
|
3138
|
+
...native === "gradle" ? { androidPackage: value.androidPackage } : {},
|
|
3139
|
+
...typeof value.sourceDigest === "string" ? { sourceDigest: value.sourceDigest } : {},
|
|
3094
3140
|
compilationCache: isJsonObject(value.compilationCache) ? value.compilationCache : {},
|
|
3095
3141
|
timings
|
|
3096
3142
|
};
|
|
@@ -3191,11 +3237,34 @@ var BuildSession = class {
|
|
|
3191
3237
|
}
|
|
3192
3238
|
async start(params) {
|
|
3193
3239
|
if (!isJsonObject(params) || !validRepo(params.repo)) return refusal("bad-request", "build.start needs params.repo.");
|
|
3194
|
-
const strings = ["
|
|
3240
|
+
const strings = ["stimBuild"];
|
|
3195
3241
|
if (strings.some((key) => typeof params[key] !== "string" || !params[key])) return refusal("bad-request", `build.start needs params.${strings.join(", params.")}.`);
|
|
3196
3242
|
const platform = params.platform;
|
|
3197
3243
|
if (platform !== "ios" && platform !== "android" && platform !== "macos") return refusal("bad-request", "params.platform must be ios, android or macos.");
|
|
3198
3244
|
if (platform === "ios" && (typeof params.runtime !== "string" || !params.runtime)) return refusal("bad-request", "An ios build needs params.runtime.");
|
|
3245
|
+
let native;
|
|
3246
|
+
if (params.native !== void 0) {
|
|
3247
|
+
const input = params.native;
|
|
3248
|
+
if (!isJsonObject(input) || typeof input.sourceDigest !== "string" || !/^[0-9a-f]{64}$/.test(input.sourceDigest)) return refusal("bad-request", "A native build needs its source transfer digest.");
|
|
3249
|
+
if (input.provider === "gradle") {
|
|
3250
|
+
const inputs = input.inputs;
|
|
3251
|
+
if (platform !== "android" || params.fingerprint !== null || !isJsonObject(inputs) || inputs.complete !== true || Object.keys(inputs).some((key) => ![
|
|
3252
|
+
"complete",
|
|
3253
|
+
"ignored",
|
|
3254
|
+
"outputs"
|
|
3255
|
+
].includes(key)) || !Array.isArray(inputs.ignored) || !inputs.ignored.every(validBuildPath) || !Array.isArray(inputs.outputs) || !inputs.outputs.every(validBuildPath)) return refusal("bad-request", "A native Gradle build needs declared complete inputs and a null artifact fingerprint.");
|
|
3256
|
+
} else {
|
|
3257
|
+
const optimizations = params.optimizations;
|
|
3258
|
+
if (platform !== "ios" || input.provider !== "xcode" || typeof input.cacheKey !== "string" || !input.cacheKey || ![
|
|
3259
|
+
null,
|
|
3260
|
+
"arm64",
|
|
3261
|
+
"x86_64"
|
|
3262
|
+
].includes(input.arch) || !isJsonObject(input.parameters) || !isJsonObject(optimizations) || typeof optimizations.compilationCache !== "boolean" || typeof optimizations.prefixMapping !== "boolean" || optimizations.swiftCompilationCache !== null && typeof optimizations.swiftCompilationCache !== "boolean") return refusal("bad-request", "A native Xcode build needs its source digest, cache key, architecture and compiler options.");
|
|
3263
|
+
}
|
|
3264
|
+
native = input;
|
|
3265
|
+
}
|
|
3266
|
+
if (native?.provider !== "gradle" && (typeof params.fingerprint !== "string" || !params.fingerprint)) return refusal("bad-request", "build.start needs params.fingerprint.");
|
|
3267
|
+
if (!native && [...this.files.values()].some((file) => file.kind === "directory")) return refusal("bad-request", "Directory entries require a native build request.");
|
|
3199
3268
|
const android = platform === "android" ? androidOptions(params.android) : null;
|
|
3200
3269
|
if (platform === "android" && !android) return refusal("bad-request", "An android build needs params.android: variant and abi (string or null), gradleBuildCache, pch and compilerCache.");
|
|
3201
3270
|
let macos = null;
|
|
@@ -3215,7 +3284,7 @@ var BuildSession = class {
|
|
|
3215
3284
|
if (!this.done || this.repo !== params.repo) return refusal("bad-request", "Send the whole manifest of this repo with build.sync first.");
|
|
3216
3285
|
if (this.incoming || [...this.files.values()].some((file) => this.expected.has(file.sha256))) return refusal("bad-request", "Some files of the manifest were not uploaded.");
|
|
3217
3286
|
for (const file of this.files.values()) if (!existsSync(this.blobPath(file.sha256))) return refusal("bad-request", `The blob of ${file.path} is missing; sync again.`);
|
|
3218
|
-
const toolchain = await this.host.toolchain();
|
|
3287
|
+
const toolchain = await this.host.toolchain(null, native?.provider);
|
|
3219
3288
|
if (this.closed) return refusal("build-refused", "The connection closed.");
|
|
3220
3289
|
if (!toolchain || toolchain.stimBuild !== params.stimBuild) return refusal("build-refused", `This Mac runs Stim build ${toolchain?.stimBuild ?? "unknown"}.`);
|
|
3221
3290
|
const launched = this.host.launch({
|
|
@@ -3226,6 +3295,7 @@ var BuildSession = class {
|
|
|
3226
3295
|
job: {
|
|
3227
3296
|
manifest: [...this.files.values()],
|
|
3228
3297
|
platform,
|
|
3298
|
+
...native ? { native } : {},
|
|
3229
3299
|
android,
|
|
3230
3300
|
macos,
|
|
3231
3301
|
project: params.project,
|
|
@@ -3233,6 +3303,7 @@ var BuildSession = class {
|
|
|
3233
3303
|
isExpo: params.isExpo === true,
|
|
3234
3304
|
configuration: optional(params.configuration),
|
|
3235
3305
|
scheme: optional(params.scheme),
|
|
3306
|
+
iosProjectPath: optional(params.iosProjectPath),
|
|
3236
3307
|
runtime: optional(params.runtime),
|
|
3237
3308
|
expectedFingerprint: params.fingerprint,
|
|
3238
3309
|
optimizations: isJsonObject(params.optimizations) ? params.optimizations : null
|
|
@@ -7100,12 +7171,13 @@ var DeviceHost = class {
|
|
|
7100
7171
|
appOffer(client, params) {
|
|
7101
7172
|
if (!this.options.allowed(client)) return refused$1("forbidden", "Current device-host approval is required.");
|
|
7102
7173
|
const offer = parseHostedAppOffer(params);
|
|
7103
|
-
if (!offer) return refused$1("bad-request", "App offers need a bounded normalized bundle manifest, bundleId, development or
|
|
7174
|
+
if (!offer) return refused$1("bad-request", "App offers need a bounded normalized bundle manifest, bundleId, development, release or process mode and, optionally, at most 32 arguments of 1024 characters (8192 in total) without NUL or line breaks.");
|
|
7104
7175
|
try {
|
|
7105
7176
|
const record = this.appSession(client, offer);
|
|
7177
|
+
if (offer.mode === "process" && record.platform === "macos") throw new Error("Process-mode app offers require a hosted iOS or Android session.");
|
|
7106
7178
|
if (record.platform !== "macos" && offer.arguments !== void 0) throw new Error("App arguments are supported only for hosted macOS sessions.");
|
|
7107
7179
|
if (record.platform === "macos") {
|
|
7108
|
-
if (offer.mode
|
|
7180
|
+
if (offer.mode !== "release" || offer.devClientScheme !== void 0) throw new Error("Hosted macOS apps require release mode without a development client scheme.");
|
|
7109
7181
|
if (offer.bundleId.startsWith("com.apple.")) throw new Error("Hosted macOS apps cannot use a com.apple. bundle identity.");
|
|
7110
7182
|
if (hostedMacosBundleId(offer.bundleId, record.appSlot).length + 6 > 255) throw new Error("The hosted macOS bundle identity exceeds 249 characters for its preferences plist.");
|
|
7111
7183
|
}
|
|
@@ -11539,16 +11611,28 @@ const INTERVAL_MS = 15e3;
|
|
|
11539
11611
|
const POINTS = 40;
|
|
11540
11612
|
/**
|
|
11541
11613
|
* Per-environment and per-device CPU and memory from the status payloads stim-server receives, in 15-second slots, the cadence at which `status --watch` rereads machine usage,
|
|
11542
|
-
* over the last 10 minutes. A later payload in the same slot replaces the earlier one.
|
|
11614
|
+
* over the last 10 minutes. A later payload in the same slot replaces the earlier one. `status --watch` prints no line
|
|
11615
|
+
* while usage stays within a step, so the slots between two payloads repeat the readings of the series the earlier
|
|
11616
|
+
* payload held, and those series continue to now until a payload drops them. An environment sums every
|
|
11543
11617
|
* machine owner of its workspace; a device is a simulator or emulator owner, keyed by its UDID or AVD name.
|
|
11544
11618
|
*/
|
|
11545
11619
|
var UsageRecorder = class {
|
|
11546
11620
|
environments = /* @__PURE__ */ new Map();
|
|
11547
11621
|
devices = /* @__PURE__ */ new Map();
|
|
11622
|
+
lastSlot = 0;
|
|
11623
|
+
liveEnvironments = /* @__PURE__ */ new Set();
|
|
11624
|
+
liveDevices = /* @__PURE__ */ new Set();
|
|
11548
11625
|
record(payload, at = Date.now()) {
|
|
11549
|
-
const owners = payload?.machine?.owners;
|
|
11550
|
-
if (!Array.isArray(owners)) return;
|
|
11551
11626
|
const slot = Math.floor(at / INTERVAL_MS);
|
|
11627
|
+
const owners = payload?.machine?.owners;
|
|
11628
|
+
this.holdThrough(slot - 1);
|
|
11629
|
+
this.lastSlot = Math.max(this.lastSlot, slot);
|
|
11630
|
+
this.liveEnvironments = /* @__PURE__ */ new Set();
|
|
11631
|
+
this.liveDevices = /* @__PURE__ */ new Set();
|
|
11632
|
+
if (!Array.isArray(owners)) {
|
|
11633
|
+
this.prune(slot);
|
|
11634
|
+
return;
|
|
11635
|
+
}
|
|
11552
11636
|
const sums = /* @__PURE__ */ new Map();
|
|
11553
11637
|
for (const owner of owners) {
|
|
11554
11638
|
const reading = {
|
|
@@ -11578,20 +11662,24 @@ var UsageRecorder = class {
|
|
|
11578
11662
|
meta,
|
|
11579
11663
|
slots: slots.set(slot, reading)
|
|
11580
11664
|
});
|
|
11665
|
+
this.liveDevices.add(key);
|
|
11581
11666
|
}
|
|
11582
11667
|
}
|
|
11583
|
-
for (const [workspace, reading] of sums)
|
|
11668
|
+
for (const [workspace, reading] of sums) {
|
|
11669
|
+
this.environments.set(workspace, (this.environments.get(workspace) ?? /* @__PURE__ */ new Map()).set(slot, reading));
|
|
11670
|
+
this.liveEnvironments.add(workspace);
|
|
11671
|
+
}
|
|
11584
11672
|
this.prune(slot);
|
|
11585
11673
|
}
|
|
11586
11674
|
/** The last 10 minutes ending at `now`'s slot, or null when no series has a reading in them. */
|
|
11587
11675
|
history(now = Date.now()) {
|
|
11588
11676
|
const end = Math.floor(now / INTERVAL_MS);
|
|
11589
11677
|
this.prune(end);
|
|
11590
|
-
const series = (slots) => {
|
|
11678
|
+
const series = (slots, live) => {
|
|
11591
11679
|
const cpuPercent = [];
|
|
11592
11680
|
const memoryMb = [];
|
|
11593
11681
|
for (let slot = end - POINTS + 1; slot <= end; slot++) {
|
|
11594
|
-
const reading = slots.get(slot);
|
|
11682
|
+
const reading = slots.get(slot) ?? (live && slot > this.lastSlot ? slots.get(this.lastSlot) : void 0);
|
|
11595
11683
|
cpuPercent.push(reading ? Math.round(reading.cpuPercent * 10) / 10 : null);
|
|
11596
11684
|
memoryMb.push(reading ? Math.round(reading.memoryMb) : null);
|
|
11597
11685
|
}
|
|
@@ -11600,8 +11688,8 @@ var UsageRecorder = class {
|
|
|
11600
11688
|
memoryMb
|
|
11601
11689
|
};
|
|
11602
11690
|
};
|
|
11603
|
-
const environments = [...this.environments].map(([workspace, slots]) => Object.assign({ workspace }, series(slots)));
|
|
11604
|
-
const devices = [...this.devices
|
|
11691
|
+
const environments = [...this.environments].map(([workspace, slots]) => Object.assign({ workspace }, series(slots, this.liveEnvironments.has(workspace))));
|
|
11692
|
+
const devices = [...this.devices].map(([key, { meta, slots }]) => Object.assign({}, meta, series(slots, this.liveDevices.has(key))));
|
|
11605
11693
|
if (!environments.length && !devices.length) return null;
|
|
11606
11694
|
return {
|
|
11607
11695
|
intervalMs: INTERVAL_MS,
|
|
@@ -11610,13 +11698,27 @@ var UsageRecorder = class {
|
|
|
11610
11698
|
devices
|
|
11611
11699
|
};
|
|
11612
11700
|
}
|
|
11701
|
+
/** Ends the live series where the last payload left them: nothing is recorded, or held, while nobody listens. */
|
|
11702
|
+
pause() {
|
|
11703
|
+
this.liveEnvironments = /* @__PURE__ */ new Set();
|
|
11704
|
+
this.liveDevices = /* @__PURE__ */ new Set();
|
|
11705
|
+
}
|
|
11706
|
+
holdThrough(slot) {
|
|
11707
|
+
const hold = (slots) => {
|
|
11708
|
+
const reading = slots?.get(this.lastSlot);
|
|
11709
|
+
if (!slots || !reading) return;
|
|
11710
|
+
for (let at = Math.max(this.lastSlot + 1, slot - POINTS + 1); at <= slot; at++) slots.set(at, reading);
|
|
11711
|
+
};
|
|
11712
|
+
for (const key of this.liveEnvironments) hold(this.environments.get(key));
|
|
11713
|
+
for (const key of this.liveDevices) hold(this.devices.get(key)?.slots);
|
|
11714
|
+
}
|
|
11613
11715
|
prune(slot) {
|
|
11614
|
-
const trim = (slots) => {
|
|
11615
|
-
for (const at of slots.keys()) if (at <= slot - POINTS) slots.delete(at);
|
|
11716
|
+
const trim = (slots, live) => {
|
|
11717
|
+
for (const at of slots.keys()) if (at <= slot - POINTS && !(live && at === this.lastSlot)) slots.delete(at);
|
|
11616
11718
|
return slots.size > 0;
|
|
11617
11719
|
};
|
|
11618
|
-
for (const [key, slots] of this.environments) if (!trim(slots)) this.environments.delete(key);
|
|
11619
|
-
for (const [key, { slots }] of this.devices) if (!trim(slots)) this.devices.delete(key);
|
|
11720
|
+
for (const [key, slots] of this.environments) if (!trim(slots, this.liveEnvironments.has(key))) this.environments.delete(key);
|
|
11721
|
+
for (const [key, { slots }] of this.devices) if (!trim(slots, this.liveDevices.has(key))) this.devices.delete(key);
|
|
11620
11722
|
}
|
|
11621
11723
|
};
|
|
11622
11724
|
//#endregion
|
|
@@ -12451,6 +12553,7 @@ var ServerUpdates = class {
|
|
|
12451
12553
|
running = null;
|
|
12452
12554
|
upload = null;
|
|
12453
12555
|
idle = null;
|
|
12556
|
+
ended = null;
|
|
12454
12557
|
constructor(options) {
|
|
12455
12558
|
this.options = options;
|
|
12456
12559
|
this.sweepIncoming();
|
|
@@ -12504,9 +12607,14 @@ var ServerUpdates = class {
|
|
|
12504
12607
|
service: service ? label : null,
|
|
12505
12608
|
acceptsClientBuilds: this.options.acceptsClientBuilds(),
|
|
12506
12609
|
running: this.progress(),
|
|
12507
|
-
last: service && label ? readLastUpdate(label) : null
|
|
12610
|
+
last: service && label ? this.latest(readLastUpdate(label)) : null
|
|
12508
12611
|
};
|
|
12509
12612
|
}
|
|
12613
|
+
latest(recorded) {
|
|
12614
|
+
if (!this.ended) return recorded;
|
|
12615
|
+
if (!recorded) return this.ended;
|
|
12616
|
+
return Date.parse(recorded.at) >= Date.parse(this.ended.at) ? recorded : this.ended;
|
|
12617
|
+
}
|
|
12510
12618
|
async start(by, params) {
|
|
12511
12619
|
const parsed = parseUpdateStart(params);
|
|
12512
12620
|
if (typeof parsed === "string") return refused("bad-request", parsed);
|
|
@@ -12665,6 +12773,12 @@ var ServerUpdates = class {
|
|
|
12665
12773
|
const settle = (ok, message) => {
|
|
12666
12774
|
this.options.drain(null);
|
|
12667
12775
|
this.clear();
|
|
12776
|
+
this.ended = {
|
|
12777
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12778
|
+
target,
|
|
12779
|
+
ok,
|
|
12780
|
+
message
|
|
12781
|
+
};
|
|
12668
12782
|
this.options.audit({
|
|
12669
12783
|
by: running.by,
|
|
12670
12784
|
target,
|
|
@@ -12677,7 +12791,11 @@ var ServerUpdates = class {
|
|
|
12677
12791
|
child.once("exit", (code) => {
|
|
12678
12792
|
const last = readLastUpdate(this.options.label);
|
|
12679
12793
|
const fresh = last && Date.parse(last.at) >= Date.parse(running.startedAt);
|
|
12680
|
-
|
|
12794
|
+
let reason = "";
|
|
12795
|
+
try {
|
|
12796
|
+
reason = readFileSync(log, "utf8").split("\n").findLast((line) => line.trim() !== "") ?? "";
|
|
12797
|
+
} catch {}
|
|
12798
|
+
settle(code === 0, fresh ? last.message : `The update exited with code ${String(code)}${reason ? `: ${reason}` : ""}; see ${log}.`);
|
|
12681
12799
|
});
|
|
12682
12800
|
return null;
|
|
12683
12801
|
}
|
|
@@ -13168,6 +13286,28 @@ function peerAddress(request) {
|
|
|
13168
13286
|
const forwarded = request.headers["x-forwarded-for"];
|
|
13169
13287
|
return (Array.isArray(forwarded) ? forwarded[0] : forwarded)?.split(",")[0]?.trim() || null;
|
|
13170
13288
|
}
|
|
13289
|
+
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
13290
|
+
"127.0.0.1",
|
|
13291
|
+
"localhost",
|
|
13292
|
+
"[::1]"
|
|
13293
|
+
]);
|
|
13294
|
+
/**
|
|
13295
|
+
* Whether a WebSocket upgrade comes from a web page. Browsers send `Sec-Fetch-Site` and the page's own `Origin`.
|
|
13296
|
+
* React Native derives `Origin` from the ws URL, so an app's `Origin` names the requested host: over `http:` on loopback,
|
|
13297
|
+
* and `https:` through `tailscale serve`. SocketRocket omits the brackets of an IPv6 origin, so an iOS app on
|
|
13298
|
+
* `ws://[::1]` is refused.
|
|
13299
|
+
*/
|
|
13300
|
+
function upgradeFromWebPage(headers, local) {
|
|
13301
|
+
if (headers["sec-fetch-site"] !== void 0) return true;
|
|
13302
|
+
if (headers.origin === void 0) return false;
|
|
13303
|
+
const scheme = local ? "http:" : "https:";
|
|
13304
|
+
try {
|
|
13305
|
+
const own = new URL(`${scheme}//${headers.host ?? ""}`);
|
|
13306
|
+
return new URL(headers.origin).origin !== own.origin || local && !LOOPBACK_HOSTS.has(own.hostname);
|
|
13307
|
+
} catch {
|
|
13308
|
+
return true;
|
|
13309
|
+
}
|
|
13310
|
+
}
|
|
13171
13311
|
var FailureLimiter = class {
|
|
13172
13312
|
failures = /* @__PURE__ */ new Map();
|
|
13173
13313
|
max;
|
|
@@ -13415,6 +13555,7 @@ async function startServer(options) {
|
|
|
13415
13555
|
if (recorders > 0) return;
|
|
13416
13556
|
stopRecording?.();
|
|
13417
13557
|
stopRecording = null;
|
|
13558
|
+
usage.pause();
|
|
13418
13559
|
};
|
|
13419
13560
|
};
|
|
13420
13561
|
const controllers = /* @__PURE__ */ new Map();
|
|
@@ -13774,8 +13915,7 @@ async function startServer(options) {
|
|
|
13774
13915
|
server: {
|
|
13775
13916
|
name: options.name,
|
|
13776
13917
|
version: options.serverVersion,
|
|
13777
|
-
stim: options.stimVersion
|
|
13778
|
-
home: homedir()
|
|
13918
|
+
stim: options.stimVersion
|
|
13779
13919
|
},
|
|
13780
13920
|
capabilities: [],
|
|
13781
13921
|
features: [...protocol_exports.FEATURES],
|
|
@@ -15163,6 +15303,10 @@ async function startServer(options) {
|
|
|
15163
15303
|
socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
15164
15304
|
return;
|
|
15165
15305
|
}
|
|
15306
|
+
if (upgradeFromWebPage(request.headers, peer === null)) {
|
|
15307
|
+
socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
15308
|
+
return;
|
|
15309
|
+
}
|
|
15166
15310
|
if (limiter.blocked(peer ?? "local")) {
|
|
15167
15311
|
socket.end("HTTP/1.1 429 Too Many Requests\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
15168
15312
|
return;
|
|
@@ -15760,7 +15904,7 @@ async function main() {
|
|
|
15760
15904
|
const service = command === "service";
|
|
15761
15905
|
if ((values.env || values["path-prepend"]) && command !== void 0 && !service) fail(`--env and --path-prepend apply only to serving and \`service install\`.\n${USAGE}`);
|
|
15762
15906
|
if (values.env || values["path-prepend"]) {
|
|
15763
|
-
const problem = validateServeEnvironment(extraEnv, pathPrepend);
|
|
15907
|
+
const problem = validateServeEnvironment(extraEnv, pathPrepend, command === void 0);
|
|
15764
15908
|
if (problem) fail(problem);
|
|
15765
15909
|
}
|
|
15766
15910
|
if ((values.label !== void 0 || values.serve) && !service) fail(`--label and --serve apply only to \`service\`.\n${USAGE}`);
|
package/dist/stim-server.mjs
CHANGED
|
@@ -4,6 +4,6 @@ const encode = (version) => version.split(".", 3).reduce((sum, part) => sum * 1e
|
|
|
4
4
|
if (encode(process.versions.node) < encode("22.12.0")) {
|
|
5
5
|
process.stderr.write(`STIM_NODE_UNSUPPORTED: stim-server needs Node 22.12.0 or later; this is Node ${process.versions.node} at ${process.execPath}. To run stim-server where a project pins an older Node, see https://stim.appandflow.com/docs/requirements#older-node-pins\n`);
|
|
6
6
|
process.exitCode = 1;
|
|
7
|
-
} else import("./stim-server-
|
|
7
|
+
} else import("./stim-server-C5iKxjba.mjs");
|
|
8
8
|
//#endregion
|
|
9
9
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stim-cli/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "Serves Stim status to paired clients over Tailscale, and runs reload and stop for clients granted control.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"./protocol.schema.json": "./dist/protocol.schema.json"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@stim-cli/core": "^1.
|
|
32
|
-
"stim": "1.
|
|
31
|
+
"@stim-cli/core": "^1.20.0",
|
|
32
|
+
"stim": "1.20.0",
|
|
33
33
|
"ws": "^8.21.3"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|