@stim-cli/server 1.12.0 → 1.13.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 +19 -10
- package/dist/{protocol-BElQc2vm.mjs → protocol-rC-5KT1O.mjs} +17 -3
- package/dist/protocol.d.mts +12 -5
- package/dist/protocol.mjs +2 -2
- package/dist/protocol.schema.json +9 -1
- package/dist/stim-frames/main.swift +47 -18
- package/dist/stim-server.mjs +84 -41
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -165,23 +165,31 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
165
165
|
`$STIM_HOME/server/helpers/`, named by a hash of the sources and the
|
|
166
166
|
compiler version. For a simulator it renders the display's framebuffer
|
|
167
167
|
(CoreSimulator's IOSurface) when the display reports damage, turned
|
|
168
|
-
upright
|
|
168
|
+
upright. An iPhone Duo lights one of its two panels, the cover while
|
|
169
|
+
folded and the inner panel while unfolded, and leaves the other black, so
|
|
170
|
+
the helper streams whichever panel is lit; its frames and video carry
|
|
171
|
+
`posture`, and the size changes with the panel. For an emulator it keeps one gRPC `streamScreenshot` call open,
|
|
169
172
|
found through the discovery file and token the emulator writes when Stim
|
|
170
173
|
boots it. It scales frames to fit the largest `maxEdge` and paces them to
|
|
171
174
|
the highest `fps` its subscribers asked for. All subscribers of a device
|
|
172
|
-
share one helper, which sends a new subscriber the latest frame
|
|
173
|
-
|
|
175
|
+
share one helper, which sends a new subscriber the latest frame. It keeps
|
|
176
|
+
running for 10 seconds after the last subscriber leaves, so a client that
|
|
177
|
+
subscribes for one frame at a time reuses it and gets the latest frame at
|
|
178
|
+
once, and exits after that or when the server's end of its stdin closes.
|
|
174
179
|
|
|
175
180
|
A client that decodes H.264 adds `video: ["h264"]`. When the helper is
|
|
176
181
|
built, the result carries `video: "h264"`, `fps` may go up to 60, and
|
|
177
182
|
frames arrive as binary WebSocket messages instead of `frame` events: a
|
|
178
183
|
big-endian header (u8 version 1, u8 flags with bit 0 set on a keyframe,
|
|
184
|
+
and on an iPhone Duo bit 1 while folded or bit 2 while unfolded,
|
|
179
185
|
u16 header length, u32 sequence number of the messages sent on this
|
|
180
186
|
subscription, f64 capture time in milliseconds since the epoch on the
|
|
181
187
|
Mac's clock, u16 width, u16 height, u8 subscription id length and the
|
|
182
188
|
ASCII id), then one Annex-B access unit. Every keyframe carries its SPS
|
|
183
189
|
and PPS, and the stream has no B-frames, so each access unit is shown as
|
|
184
|
-
it arrives.
|
|
190
|
+
it arrives. A change of size, such as a rotation or a Duo fold, restarts
|
|
191
|
+
the encoder, and the next access unit is a keyframe with the new SPS and
|
|
192
|
+
PPS. The helper encodes with VideoToolbox in real time: Main
|
|
185
193
|
profile, a keyframe at least every 2 seconds, straight from the
|
|
186
194
|
simulator's IOSurface or from the emulator's RGBA frames, only when the
|
|
187
195
|
screen changed. A subscriber starts at a keyframe, and `frames.keyframe`
|
|
@@ -195,13 +203,13 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
195
203
|
subscribers of the same device still get at most the `fps` they asked
|
|
196
204
|
for. Watching video needs only `read`. Without the helper, the result has
|
|
197
205
|
no `video`, `fps` above 30 is lowered to 30, and `frame` events arrive as
|
|
198
|
-
before;
|
|
199
|
-
|
|
206
|
+
before; a helper that fails before its first frame also falls back to
|
|
207
|
+
`frame` events within a video subscription.
|
|
200
208
|
|
|
201
209
|
Without the helper (the compiler is missing or fails, which the server
|
|
202
|
-
retries every 5 minutes, or the helper fails before its first frame),
|
|
203
|
-
subscription made while it is still being built,
|
|
204
|
-
|
|
210
|
+
retries every 5 minutes, or the helper fails before its first frame), and
|
|
211
|
+
for a subscription made while it is still being built, frames come from
|
|
212
|
+
screenshots, and `fps` and `maxEdge` only cap the rate. Simulators are
|
|
205
213
|
captured with `xcrun simctl io <udid> screenshot`, of the primary display,
|
|
206
214
|
or of the default display when that `simctl` does not accept `primary`. An
|
|
207
215
|
iPhone Duo lights one of two panels: the capture follows the lit one
|
|
@@ -365,7 +373,8 @@ streams its frames:
|
|
|
365
373
|
through gRPC `setPhysicalModel` for rotation and `setPosture` for its hinge.
|
|
366
374
|
An iPhone Duo folds with `sim-fold`, which the server builds from Stim
|
|
367
375
|
Desktop's sources on the first fold and runs inside the simulator with
|
|
368
|
-
`xcrun simctl spawn`; it answers `{}` once the fold finished
|
|
376
|
+
`xcrun simctl spawn`; it answers `{}` once the fold finished, and fails
|
|
377
|
+
with `action-failed` when the fold does not finish within 40 seconds. `sim-fold`
|
|
369
378
|
swaps the posture, so the server runs it only when the Duo's latest frame
|
|
370
379
|
shows the other posture, and refuses the request until a frame showed one.
|
|
371
380
|
|
|
@@ -67,6 +67,12 @@ const LOG_LEVELS = [
|
|
|
67
67
|
"fatal"
|
|
68
68
|
];
|
|
69
69
|
const MAX_LOG_TAIL = 5e3;
|
|
70
|
+
/** `reload` also reaches the workspace's Stim-owned Chrome page. */
|
|
71
|
+
const RELOAD_PLATFORMS = [
|
|
72
|
+
"ios",
|
|
73
|
+
"android",
|
|
74
|
+
"web"
|
|
75
|
+
];
|
|
70
76
|
const FRAME_FPS = {
|
|
71
77
|
default: 5,
|
|
72
78
|
max: 30,
|
|
@@ -81,6 +87,9 @@ const VIDEO_CODECS = ["h264"];
|
|
|
81
87
|
const VIDEO_HEADER_VERSION = 1;
|
|
82
88
|
/** The keyframe bit of a {@link VideoPacket}'s flags. */
|
|
83
89
|
const VIDEO_KEYFRAME = 1;
|
|
90
|
+
/** The flag bits of a {@link VideoPacket} that carry an iPhone Duo's posture, as `posture` on a `frame` event. */
|
|
91
|
+
const VIDEO_FOLDED = 2;
|
|
92
|
+
const VIDEO_UNFOLDED = 4;
|
|
84
93
|
/** Each action runs one fixed `stim` command in the workspace. */
|
|
85
94
|
const ACTIONS = ["reload", "stop"];
|
|
86
95
|
const TOUCH_PHASES = [
|
|
@@ -263,7 +272,12 @@ function protocolJsonSchema() {
|
|
|
263
272
|
ts: { type: "number" },
|
|
264
273
|
src: { type: "string" },
|
|
265
274
|
level: { type: "string" },
|
|
266
|
-
msg: { type: "string" }
|
|
275
|
+
msg: { type: "string" },
|
|
276
|
+
context: {
|
|
277
|
+
type: "array",
|
|
278
|
+
items: { type: "string" },
|
|
279
|
+
description: "With `errors`: the code frame and stack lines Expo printed after this error."
|
|
280
|
+
}
|
|
267
281
|
}
|
|
268
282
|
},
|
|
269
283
|
LogFilter: {
|
|
@@ -343,7 +357,7 @@ function protocolJsonSchema() {
|
|
|
343
357
|
type: "string",
|
|
344
358
|
description: "An environment path from a status payload."
|
|
345
359
|
},
|
|
346
|
-
platform: { enum: [
|
|
360
|
+
platform: { enum: [...RELOAD_PLATFORMS] }
|
|
347
361
|
}
|
|
348
362
|
}, {
|
|
349
363
|
type: "object",
|
|
@@ -805,4 +819,4 @@ function protocolJsonSchema() {
|
|
|
805
819
|
};
|
|
806
820
|
}
|
|
807
821
|
//#endregion
|
|
808
|
-
export {
|
|
822
|
+
export { VIDEO_UNFOLDED as C, VIDEO_KEYFRAME as S, ROTATE_DIRECTIONS as _, ERROR_CODES as a, VIDEO_FOLDED as b, INPUT_BUTTONS as c, MAX_INPUT_TEXT as d, MAX_LOG_TAIL as f, RELOAD_PLATFORMS as g, PROTOCOL_VERSION as h, DEVICE_POSTURES as i, LOG_LEVELS as l, PROTOCOL_SCHEMA_FILE as m, CAPABILITIES as n, FRAME_EDGE as o, METHODS as p, CONTROL_END_REASONS as r, FRAME_FPS as s, ACTIONS as t, LOG_SOURCES as u, TOUCH_PHASES as v, protocolJsonSchema as w, VIDEO_HEADER_VERSION as x, VIDEO_CODECS as y };
|
package/dist/protocol.d.mts
CHANGED
|
@@ -93,6 +93,9 @@ type StatsResult = Record<string, unknown>;
|
|
|
93
93
|
/** `stim settings --json`; the CLI masks sensitive values. */
|
|
94
94
|
type SettingsResult = Record<string, unknown>;
|
|
95
95
|
type Platform = "ios" | "android";
|
|
96
|
+
/** `reload` also reaches the workspace's Stim-owned Chrome page. */
|
|
97
|
+
declare const RELOAD_PLATFORMS: readonly ["ios", "android", "web"];
|
|
98
|
+
type ReloadPlatform = (typeof RELOAD_PLATFORMS)[number];
|
|
96
99
|
declare const FRAME_FPS: {
|
|
97
100
|
readonly default: 5;
|
|
98
101
|
readonly max: 30;
|
|
@@ -122,7 +125,7 @@ declare const VIDEO_CODECS: readonly ["h264"];
|
|
|
122
125
|
type VideoCodec = (typeof VIDEO_CODECS)[number];
|
|
123
126
|
/**
|
|
124
127
|
* With `video`, frames arrive as binary WebSocket messages, one H.264 access unit each; see {@link VideoPacket}.
|
|
125
|
-
* A subscription whose device
|
|
128
|
+
* A subscription whose device falls back to screenshots still sends JSON `frame` events.
|
|
126
129
|
*/
|
|
127
130
|
interface FramesSubscribeResult extends SubscribeResult {
|
|
128
131
|
video?: VideoCodec;
|
|
@@ -134,9 +137,12 @@ interface KeyframeParams {
|
|
|
134
137
|
declare const VIDEO_HEADER_VERSION = 1;
|
|
135
138
|
/** The keyframe bit of a {@link VideoPacket}'s flags. */
|
|
136
139
|
declare const VIDEO_KEYFRAME = 1;
|
|
140
|
+
/** The flag bits of a {@link VideoPacket} that carry an iPhone Duo's posture, as `posture` on a `frame` event. */
|
|
141
|
+
declare const VIDEO_FOLDED = 2;
|
|
142
|
+
declare const VIDEO_UNFOLDED = 4;
|
|
137
143
|
/**
|
|
138
144
|
* The layout of a binary video message, big-endian: u8 version ({@link VIDEO_HEADER_VERSION}), u8 flags
|
|
139
|
-
* ({@link VIDEO_KEYFRAME}), u16 header length, u32 sequence number of the messages sent on this subscription, f64 capture time in milliseconds since the
|
|
145
|
+
* ({@link VIDEO_KEYFRAME}, and on an iPhone Duo {@link VIDEO_FOLDED} or {@link VIDEO_UNFOLDED}), u16 header length, u32 sequence number of the messages sent on this subscription, f64 capture time in milliseconds since the
|
|
140
146
|
* epoch on the Mac's clock, u16 width, u16 height, u8 subscription id length N, N bytes of ASCII subscription
|
|
141
147
|
* id. After the header comes one Annex-B H.264 access unit; a keyframe carries its SPS and PPS. The stream has
|
|
142
148
|
* no B-frames, so each access unit is shown as it arrives.
|
|
@@ -148,19 +154,20 @@ interface VideoPacket {
|
|
|
148
154
|
capturedAt: number;
|
|
149
155
|
width: number;
|
|
150
156
|
height: number;
|
|
157
|
+
posture?: "folded" | "unfolded";
|
|
151
158
|
accessUnit: Uint8Array;
|
|
152
159
|
}
|
|
153
160
|
/** Each action runs one fixed `stim` command in the workspace. */
|
|
154
161
|
declare const ACTIONS: readonly ["reload", "stop"];
|
|
155
162
|
type ActionName = (typeof ACTIONS)[number];
|
|
156
163
|
/**
|
|
157
|
-
* `reload` runs `stim reload --json`, with `platform` when
|
|
164
|
+
* `reload` runs `stim reload --json`, with `platform` when more than one platform is live; `stop` runs
|
|
158
165
|
* `stim stop --json`. `workspace` is an environment `path` from a status payload. Needs `control`.
|
|
159
166
|
*/
|
|
160
167
|
type ActionParams = {
|
|
161
168
|
action: "reload";
|
|
162
169
|
workspace: string;
|
|
163
|
-
platform?:
|
|
170
|
+
platform?: ReloadPlatform;
|
|
164
171
|
} | {
|
|
165
172
|
action: "stop";
|
|
166
173
|
workspace: string;
|
|
@@ -468,4 +475,4 @@ type JsonSchema = Record<string, unknown>;
|
|
|
468
475
|
/** The JSON Schema of every message on the wire, written to `dist/protocol.schema.json` at build time. */
|
|
469
476
|
declare function protocolJsonSchema(): JsonSchema;
|
|
470
477
|
//#endregion
|
|
471
|
-
export { ACTIONS, ActionName, ActionParams, ActionResult, BuildPlanParams, BuildPlanResult, CAPABILITIES, CONTROL_END_REASONS, Capability, ClientRequest, ControlBeginParams, ControlBeginResult, ControlEndParams, ControlEndedEvent, DEVICE_POSTURES, DeviceAuth, DevicePosture, ERROR_CODES, ErrorCode, ErrorEvent, FRAME_EDGE, FRAME_FPS, FrameDelayedEvent, FrameEvent, FrameTarget, FramesSubscribeResult, HelloParams, HelloResult, INPUT_BUTTONS, InputButton, InputButtonParams, InputPostureParams, InputRotateParams, InputTextParams, InputTouchParams, KeyframeParams, LOG_LEVELS, LOG_SOURCES, LogFilter, LogLevel, LogRecord, LogSource, LogsEvent, LogsQueryResult, MAX_INPUT_TEXT, MAX_LOG_TAIL, METHODS, MachineHistory, MachineHistoryParams, MachineUsage, MachineVolume, MemoryPressure, Method, Methods, PROTOCOL_SCHEMA_FILE, PROTOCOL_VERSION, PairingAuth, Platform, ProtocolError, ROTATE_DIRECTIONS, RequestId, RotateDirection, ServerEvent, ServerMessage, ServerResponse, SettingsResult, StatsResult, StatusEvent, SubscribeResult, TOUCH_PHASES, TouchPhase, UnsubscribeParams, UsageSample, VIDEO_CODECS, VIDEO_HEADER_VERSION, VIDEO_KEYFRAME, VideoCodec, VideoPacket, WorkspaceParams, protocolJsonSchema };
|
|
478
|
+
export { ACTIONS, ActionName, ActionParams, ActionResult, BuildPlanParams, BuildPlanResult, CAPABILITIES, CONTROL_END_REASONS, Capability, ClientRequest, ControlBeginParams, ControlBeginResult, ControlEndParams, ControlEndedEvent, DEVICE_POSTURES, DeviceAuth, DevicePosture, ERROR_CODES, ErrorCode, ErrorEvent, FRAME_EDGE, FRAME_FPS, FrameDelayedEvent, FrameEvent, FrameTarget, FramesSubscribeResult, HelloParams, HelloResult, INPUT_BUTTONS, InputButton, InputButtonParams, InputPostureParams, InputRotateParams, InputTextParams, InputTouchParams, KeyframeParams, LOG_LEVELS, LOG_SOURCES, LogFilter, LogLevel, LogRecord, LogSource, LogsEvent, LogsQueryResult, MAX_INPUT_TEXT, MAX_LOG_TAIL, METHODS, MachineHistory, MachineHistoryParams, MachineUsage, MachineVolume, MemoryPressure, Method, Methods, PROTOCOL_SCHEMA_FILE, PROTOCOL_VERSION, PairingAuth, Platform, ProtocolError, RELOAD_PLATFORMS, ROTATE_DIRECTIONS, ReloadPlatform, RequestId, RotateDirection, ServerEvent, ServerMessage, ServerResponse, SettingsResult, StatsResult, StatusEvent, SubscribeResult, TOUCH_PHASES, TouchPhase, UnsubscribeParams, UsageSample, VIDEO_CODECS, VIDEO_FOLDED, VIDEO_HEADER_VERSION, VIDEO_KEYFRAME, VIDEO_UNFOLDED, VideoCodec, VideoPacket, WorkspaceParams, protocolJsonSchema };
|
package/dist/protocol.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as
|
|
2
|
-
export { ACTIONS, CAPABILITIES, CONTROL_END_REASONS, DEVICE_POSTURES, ERROR_CODES, FRAME_EDGE, FRAME_FPS, INPUT_BUTTONS, LOG_LEVELS, LOG_SOURCES, MAX_INPUT_TEXT, MAX_LOG_TAIL, METHODS, PROTOCOL_SCHEMA_FILE, PROTOCOL_VERSION, ROTATE_DIRECTIONS, TOUCH_PHASES, VIDEO_CODECS, VIDEO_HEADER_VERSION, VIDEO_KEYFRAME, protocolJsonSchema };
|
|
1
|
+
import { C as VIDEO_UNFOLDED, S as VIDEO_KEYFRAME, _ as ROTATE_DIRECTIONS, a as ERROR_CODES, b as VIDEO_FOLDED, c as INPUT_BUTTONS, d as MAX_INPUT_TEXT, f as MAX_LOG_TAIL, g as RELOAD_PLATFORMS, h as PROTOCOL_VERSION, i as DEVICE_POSTURES, l as LOG_LEVELS, m as PROTOCOL_SCHEMA_FILE, n as CAPABILITIES, o as FRAME_EDGE, p as METHODS, r as CONTROL_END_REASONS, s as FRAME_FPS, t as ACTIONS, u as LOG_SOURCES, v as TOUCH_PHASES, w as protocolJsonSchema, x as VIDEO_HEADER_VERSION, y as VIDEO_CODECS } from "./protocol-rC-5KT1O.mjs";
|
|
2
|
+
export { ACTIONS, CAPABILITIES, CONTROL_END_REASONS, DEVICE_POSTURES, ERROR_CODES, FRAME_EDGE, FRAME_FPS, INPUT_BUTTONS, LOG_LEVELS, LOG_SOURCES, MAX_INPUT_TEXT, MAX_LOG_TAIL, METHODS, PROTOCOL_SCHEMA_FILE, PROTOCOL_VERSION, RELOAD_PLATFORMS, ROTATE_DIRECTIONS, TOUCH_PHASES, VIDEO_CODECS, VIDEO_FOLDED, VIDEO_HEADER_VERSION, VIDEO_KEYFRAME, VIDEO_UNFOLDED, protocolJsonSchema };
|
|
@@ -196,6 +196,13 @@
|
|
|
196
196
|
},
|
|
197
197
|
"msg": {
|
|
198
198
|
"type": "string"
|
|
199
|
+
},
|
|
200
|
+
"context": {
|
|
201
|
+
"type": "array",
|
|
202
|
+
"items": {
|
|
203
|
+
"type": "string"
|
|
204
|
+
},
|
|
205
|
+
"description": "With `errors`: the code frame and stack lines Expo printed after this error."
|
|
199
206
|
}
|
|
200
207
|
}
|
|
201
208
|
},
|
|
@@ -318,7 +325,8 @@
|
|
|
318
325
|
"platform": {
|
|
319
326
|
"enum": [
|
|
320
327
|
"ios",
|
|
321
|
-
"android"
|
|
328
|
+
"android",
|
|
329
|
+
"web"
|
|
322
330
|
]
|
|
323
331
|
}
|
|
324
332
|
}
|
|
@@ -21,8 +21,9 @@ import IOSurface
|
|
|
21
21
|
// keyboard.
|
|
22
22
|
// stdout carries messages framed as a 4-byte big-endian length, then a kind byte:
|
|
23
23
|
// 1 is a frame (2-byte width, 2-byte height, JPEG bytes), 2 is a JSON notice
|
|
24
|
-
// ({"error": message} before a failed exit, {"inputError": message},
|
|
25
|
-
// {"
|
|
24
|
+
// ({"error": message} before a failed exit, {"inputError": message}, on a simulator
|
|
25
|
+
// with several displays {"display": n} when display n is the one lit and streamed, or
|
|
26
|
+
// on an emulator {"keyboard": "yes|no"} once it reports its hardware), 3 is an H.264 access unit (1-byte
|
|
26
27
|
// flags with bit 0 set on a keyframe, 8-byte big-endian float capture time in
|
|
27
28
|
// milliseconds since the epoch, 2-byte width, 2-byte height, Annex-B bytes).
|
|
28
29
|
// The helper exits when stdin closes.
|
|
@@ -88,7 +89,7 @@ enum Output {
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
static func notice(_ object: [String:
|
|
92
|
+
static func notice(_ object: [String: Any]) {
|
|
92
93
|
guard let json = try? JSONSerialization.data(withJSONObject: object) else { return }
|
|
93
94
|
writer.sync { write(Data([2]) + json) }
|
|
94
95
|
}
|
|
@@ -191,7 +192,9 @@ final class SimulatorSource {
|
|
|
191
192
|
let inputQueue = DispatchQueue(label: "stim.frames.input")
|
|
192
193
|
var hid: SimulatorHID?
|
|
193
194
|
private let pacer: Pacer
|
|
194
|
-
private var
|
|
195
|
+
private var displays: [SimDisplay] = []
|
|
196
|
+
private var displayIndex = 0
|
|
197
|
+
private var reportedDisplay: Int?
|
|
195
198
|
private let callbackID = NSUUID()
|
|
196
199
|
private let video = videoEncoder()
|
|
197
200
|
private let jpegGate = JpegGate()
|
|
@@ -206,37 +209,63 @@ final class SimulatorSource {
|
|
|
206
209
|
var queue: DispatchQueue { pacer.queue }
|
|
207
210
|
|
|
208
211
|
func start(deadline: Date = Date().addingTimeInterval(30)) {
|
|
209
|
-
|
|
212
|
+
let found = CoreSimulator.displays(udid: udid)
|
|
213
|
+
guard !found.isEmpty else {
|
|
210
214
|
if Date() > deadline { fail("Simulator \(udid) has no display with a framebuffer. Is it booted?") }
|
|
211
215
|
queue.asyncAfter(deadline: .now() + 1) { self.start(deadline: deadline) }
|
|
212
216
|
return
|
|
213
217
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
display
|
|
217
|
-
|
|
218
|
+
displays = found
|
|
219
|
+
displayIndex = 0
|
|
220
|
+
for display in found {
|
|
221
|
+
display.registerDamageCallback(callbackID) { [weak self] _ in self?.pacer.changed() }
|
|
222
|
+
display.registerSurfacesCallback(callbackID) { [weak self] _ in self?.pacer.changed() }
|
|
223
|
+
display.registerPropertiesCallback(callbackID) { [weak self] _ in self?.pacer.changed() }
|
|
224
|
+
}
|
|
218
225
|
pacer.changed()
|
|
219
|
-
watch(
|
|
226
|
+
watch()
|
|
220
227
|
}
|
|
221
228
|
|
|
222
229
|
// A simulator that shuts down and boots again gets new display objects, and
|
|
223
230
|
// the old ones stop reporting damage; so does its HID client. CoreSimulator
|
|
224
231
|
// returns a new proxy for the same display on every lookup, so the watch
|
|
225
232
|
// follows the device state instead, where 3 is SimDeviceStateBooted.
|
|
226
|
-
private func watch(
|
|
233
|
+
private func watch() {
|
|
227
234
|
queue.asyncAfter(deadline: .now() + 2) {
|
|
228
235
|
guard CoreSimulator.device(udid: self.udid)?.value(forKey: "state") as? Int != 3 else {
|
|
229
|
-
|
|
236
|
+
if self.displays.count > 1 { self.pacer.changed() }
|
|
237
|
+
return self.watch()
|
|
238
|
+
}
|
|
239
|
+
for display in self.displays {
|
|
240
|
+
display.unregisterDamageCallback(self.callbackID)
|
|
241
|
+
display.unregisterSurfacesCallback(self.callbackID)
|
|
242
|
+
display.unregisterPropertiesCallback(self.callbackID)
|
|
230
243
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
display.unregisterPropertiesCallback(self.callbackID)
|
|
234
|
-
self.display = nil
|
|
244
|
+
self.displays = []
|
|
245
|
+
self.reportedDisplay = nil
|
|
235
246
|
self.inputQueue.async { self.hid = nil }
|
|
236
247
|
self.start()
|
|
237
248
|
}
|
|
238
249
|
}
|
|
239
250
|
|
|
251
|
+
// CoreSimulator keeps the built-in display an iPhone Duo's posture turned
|
|
252
|
+
// off, the cover or the inner one, all black, and sends no damage when it
|
|
253
|
+
// turns off, so watch() also re-renders a device with several displays.
|
|
254
|
+
private func litDisplay() -> SimDisplay? {
|
|
255
|
+
let panels = displays.indices.filter { displays[$0].screenProperties?.screenType == 0 }
|
|
256
|
+
guard panels.count > 1 else { return displays.first }
|
|
257
|
+
let lit = { (index: Int) in self.displays[index].framebufferSurface.map { !isBlack($0) } ?? false }
|
|
258
|
+
guard let index = lit(displayIndex) ? displayIndex : panels.first(where: lit) else {
|
|
259
|
+
return displays[displayIndex]
|
|
260
|
+
}
|
|
261
|
+
displayIndex = index
|
|
262
|
+
if reportedDisplay != index {
|
|
263
|
+
reportedDisplay = index
|
|
264
|
+
Output.notice(["display": index])
|
|
265
|
+
}
|
|
266
|
+
return displays[index]
|
|
267
|
+
}
|
|
268
|
+
|
|
240
269
|
func configure(_ config: Config) {
|
|
241
270
|
video.configure(enabled: config.video, maxEdge: config.maxEdge, fps: Int(config.fps), bitrate: config.bitrate)
|
|
242
271
|
queue.async {
|
|
@@ -253,10 +282,10 @@ final class SimulatorSource {
|
|
|
253
282
|
// uiOrientation is a UIInterfaceOrientation; the framebuffer stays in the
|
|
254
283
|
// display's native portrait orientation, so the image is turned upright.
|
|
255
284
|
private func render(_ config: Config) {
|
|
256
|
-
guard let surface = display
|
|
285
|
+
guard let display = litDisplay(), let surface = display.framebufferSurface else { return }
|
|
257
286
|
let orientation: CGImagePropertyOrientation
|
|
258
287
|
let quarterTurns: Int
|
|
259
|
-
switch display
|
|
288
|
+
switch display.screenProperties?.uiOrientation ?? 1 {
|
|
260
289
|
case 2: (orientation, quarterTurns) = (.down, 2)
|
|
261
290
|
case 3: (orientation, quarterTurns) = (.right, 3)
|
|
262
291
|
case 4: (orientation, quarterTurns) = (.left, 1)
|
package/dist/stim-server.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { _ as
|
|
2
|
+
import { _ as ROTATE_DIRECTIONS, c as INPUT_BUTTONS, f as MAX_LOG_TAIL, g as RELOAD_PLATFORMS, l as LOG_LEVELS, n as CAPABILITIES, o as FRAME_EDGE, s as FRAME_FPS, t as ACTIONS, u as LOG_SOURCES, v as TOUCH_PHASES } from "./protocol-rC-5KT1O.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { accessSync, appendFileSync, constants, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, statfsSync, watch, writeFileSync } from "node:fs";
|
|
5
5
|
import { availableParallelism, cpus, homedir, hostname, loadavg, tmpdir, totalmem } from "node:os";
|
|
@@ -284,9 +284,9 @@ function parseAction(params) {
|
|
|
284
284
|
action,
|
|
285
285
|
workspace
|
|
286
286
|
} };
|
|
287
|
-
if (platform !== void 0 && platform
|
|
287
|
+
if (platform !== void 0 && !RELOAD_PLATFORMS.includes(platform)) return {
|
|
288
288
|
code: "bad-request",
|
|
289
|
-
message:
|
|
289
|
+
message: `platform must be one of ${RELOAD_PLATFORMS.join(", ")}.`
|
|
290
290
|
};
|
|
291
291
|
return { action: {
|
|
292
292
|
action: "reload",
|
|
@@ -397,12 +397,20 @@ function runStim(stimCli, env, args, cwd, limits) {
|
|
|
397
397
|
const chunks = [];
|
|
398
398
|
let size = 0;
|
|
399
399
|
let stderr = "";
|
|
400
|
+
const release = () => {
|
|
401
|
+
child.stdout.destroy();
|
|
402
|
+
child.stderr.destroy();
|
|
403
|
+
};
|
|
400
404
|
const fail = (message) => {
|
|
401
405
|
if (failure) return;
|
|
402
406
|
failure = message;
|
|
407
|
+
release();
|
|
403
408
|
terminate(child);
|
|
404
409
|
};
|
|
405
|
-
const timer = setTimeout(() =>
|
|
410
|
+
const timer = setTimeout(() => {
|
|
411
|
+
if (child.exitCode !== null || child.signalCode !== null) return release();
|
|
412
|
+
fail(`${label} did not finish within ${limits.timeoutMs / 1e3} s.`);
|
|
413
|
+
}, limits.timeoutMs);
|
|
406
414
|
child.stdout.on("data", (chunk) => {
|
|
407
415
|
size += chunk.length;
|
|
408
416
|
if (size > limits.maxOutputBytes) return fail(`${label} printed more than ${limits.maxOutputBytes} bytes.`);
|
|
@@ -454,7 +462,8 @@ function videoPacket(subscription, sequence, unit) {
|
|
|
454
462
|
const id = Buffer.from(subscription, "ascii");
|
|
455
463
|
const header = Buffer.alloc(FIXED_HEADER_BYTES + id.length);
|
|
456
464
|
header.writeUInt8(1, 0);
|
|
457
|
-
|
|
465
|
+
const posture = unit.posture === "folded" ? 2 : unit.posture === "unfolded" ? 4 : 0;
|
|
466
|
+
header.writeUInt8((unit.keyframe ? 1 : 0) | posture, 1);
|
|
458
467
|
header.writeUInt16BE(header.length, 2);
|
|
459
468
|
header.writeUInt32BE(sequence >>> 0, 4);
|
|
460
469
|
header.writeDoubleBE(unit.capturedAt, 8);
|
|
@@ -670,9 +679,11 @@ async function compiled(name, inputs, version, compile) {
|
|
|
670
679
|
return helper;
|
|
671
680
|
}
|
|
672
681
|
/**
|
|
673
|
-
* One `stim-frames` process per device, shared by its subscribers and stopped
|
|
682
|
+
* One `stim-frames` process per device, shared by its subscribers and stopped `lingerMs` after the last of them
|
|
683
|
+
* leaves, unchanged meanwhile, so a subscriber that comes back within that time gets the latest frame at once. It runs
|
|
674
684
|
* at the highest fps and the largest edge any subscriber asked for; each subscriber paces its own frames. The
|
|
675
|
-
* helper exits when its stdin closes, so it cannot outlive the server.
|
|
685
|
+
* helper exits when its stdin closes, so it cannot outlive the server. `lit` turns the display the helper
|
|
686
|
+
* reports it streams into a posture, which the frames and access units after it carry.
|
|
676
687
|
*/
|
|
677
688
|
var HelperSource = class {
|
|
678
689
|
listeners = /* @__PURE__ */ new Map();
|
|
@@ -687,8 +698,14 @@ var HelperSource = class {
|
|
|
687
698
|
bitrate = new Bitrate(DEFAULT_VIDEO_LIMITS, Date.now());
|
|
688
699
|
keyframeAt = -Infinity;
|
|
689
700
|
keyframeTimer = null;
|
|
690
|
-
|
|
701
|
+
lit;
|
|
702
|
+
posture;
|
|
703
|
+
lingerMs;
|
|
704
|
+
lingerTimer = null;
|
|
705
|
+
constructor(helper, device, env, ended, lingerMs, lit) {
|
|
691
706
|
this.ended = ended;
|
|
707
|
+
this.lingerMs = lingerMs;
|
|
708
|
+
this.lit = lit;
|
|
692
709
|
const id = device.platform === "ios" ? device.udid : device.serial;
|
|
693
710
|
this.child = spawn(helper, [device.platform, id], {
|
|
694
711
|
env,
|
|
@@ -712,20 +729,23 @@ var HelperSource = class {
|
|
|
712
729
|
}
|
|
713
730
|
/** A null `hint` keeps the helper running for input without asking for frames. */
|
|
714
731
|
add(listener, hint) {
|
|
732
|
+
if (this.lingerTimer) clearTimeout(this.lingerTimer);
|
|
733
|
+
this.lingerTimer = null;
|
|
715
734
|
this.listeners.set(listener, hint);
|
|
716
735
|
this.configure();
|
|
717
736
|
if (listener.video) this.keyframe();
|
|
718
737
|
else if (this.last) listener.frame(this.last);
|
|
719
738
|
return () => {
|
|
720
739
|
if (!this.listeners.delete(listener)) return;
|
|
721
|
-
if (this.listeners.size
|
|
722
|
-
else this.
|
|
740
|
+
if (this.listeners.size > 0) this.configure();
|
|
741
|
+
else this.lingerTimer = setTimeout(() => void this.stop(), this.lingerMs);
|
|
723
742
|
};
|
|
724
743
|
}
|
|
725
744
|
stop() {
|
|
726
745
|
if (this.stopped) return Promise.resolve();
|
|
727
746
|
this.stopped = true;
|
|
728
747
|
if (this.keyframeTimer) clearTimeout(this.keyframeTimer);
|
|
748
|
+
if (this.lingerTimer) clearTimeout(this.lingerTimer);
|
|
729
749
|
this.listeners.clear();
|
|
730
750
|
this.ended();
|
|
731
751
|
this.child.stdin.end();
|
|
@@ -752,6 +772,7 @@ var HelperSource = class {
|
|
|
752
772
|
if (!this.stopped) this.child.stdin.write(`${JSON.stringify(command)}\n`);
|
|
753
773
|
}
|
|
754
774
|
configure() {
|
|
775
|
+
if (this.listeners.size === 0) return;
|
|
755
776
|
const watching = [...this.listeners].flatMap(([listener, hint]) => hint ? [{
|
|
756
777
|
listener,
|
|
757
778
|
hint
|
|
@@ -802,7 +823,8 @@ var HelperSource = class {
|
|
|
802
823
|
width: body.readUInt16BE(1),
|
|
803
824
|
height: body.readUInt16BE(3),
|
|
804
825
|
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
805
|
-
data: body.subarray(5).toString("base64")
|
|
826
|
+
data: body.subarray(5).toString("base64"),
|
|
827
|
+
...this.posture ? { posture: this.posture } : {}
|
|
806
828
|
};
|
|
807
829
|
for (const listener of this.listeners.keys()) if (!listener.video) listener.frame(this.last);
|
|
808
830
|
}
|
|
@@ -813,7 +835,8 @@ var HelperSource = class {
|
|
|
813
835
|
capturedAt: body.readDoubleBE(2),
|
|
814
836
|
width: body.readUInt16BE(10),
|
|
815
837
|
height: body.readUInt16BE(12),
|
|
816
|
-
data: body.subarray(VIDEO_HEADER_BYTES)
|
|
838
|
+
data: body.subarray(VIDEO_HEADER_BYTES),
|
|
839
|
+
...this.posture ? { posture: this.posture } : {}
|
|
817
840
|
};
|
|
818
841
|
if (this.bitrate.tick(Date.now()) !== null) this.configure();
|
|
819
842
|
for (const listener of this.listeners.keys()) listener.video?.(unit);
|
|
@@ -827,6 +850,8 @@ var HelperSource = class {
|
|
|
827
850
|
if (keyboard === "yes" || keyboard === "no") this.keyboard = keyboard === "yes";
|
|
828
851
|
const inputError = notice?.inputError;
|
|
829
852
|
if (typeof inputError === "string") console.error(`stim-server: stim-frames: ${inputError}`);
|
|
853
|
+
const display = notice?.display;
|
|
854
|
+
if (this.lit && typeof display === "number") this.posture = this.lit(display);
|
|
830
855
|
} catch {}
|
|
831
856
|
}
|
|
832
857
|
};
|
|
@@ -836,7 +861,8 @@ const DEFAULT_FRAME_LIMITS = {
|
|
|
836
861
|
toolTimeoutMs: 3e4,
|
|
837
862
|
slowCaptureMs: 3e3,
|
|
838
863
|
failureBackoffMs: 3e3,
|
|
839
|
-
maxConsecutiveFailures: 3
|
|
864
|
+
maxConsecutiveFailures: 3,
|
|
865
|
+
lingerMs: 1e4
|
|
840
866
|
};
|
|
841
867
|
const MIN_INTERVAL_MS = 200;
|
|
842
868
|
const MAX_INTERVAL_MS = 1e3;
|
|
@@ -1044,6 +1070,8 @@ function simulatorCapturer(device, env, limits, litPanels) {
|
|
|
1044
1070
|
}
|
|
1045
1071
|
};
|
|
1046
1072
|
}
|
|
1073
|
+
/** The Duo's panels in the order the helper indexes CoreSimulator's displays, by screen ID. */
|
|
1074
|
+
const DUO_PANELS = ["primary", "primary-1"];
|
|
1047
1075
|
const POSTURES = {
|
|
1048
1076
|
primary: "folded",
|
|
1049
1077
|
"primary-1": "unfolded"
|
|
@@ -1401,12 +1429,15 @@ var FrameSource = class {
|
|
|
1401
1429
|
}
|
|
1402
1430
|
};
|
|
1403
1431
|
/**
|
|
1404
|
-
* One capture per device, shared by its subscribers
|
|
1405
|
-
*
|
|
1406
|
-
*
|
|
1407
|
-
*
|
|
1408
|
-
*
|
|
1409
|
-
*
|
|
1432
|
+
* One capture per device, shared by its subscribers. A `stim-frames` helper stops `lingerMs` after the last of
|
|
1433
|
+
* them leaves, so a client that takes one frame at a time reuses it; a screenshot loop stops with the last of
|
|
1434
|
+
* them. With the `stim-frames`
|
|
1435
|
+
* helper, a device streams frames as its screen changes, at the rate its subscribers ask for; an iPhone Duo
|
|
1436
|
+
* streams its lit panel, which is its posture. Without the helper, and while it is still being built, a
|
|
1437
|
+
* screenshot loop sends a frame only when the screen changed: up to 5 times a second while it changes, backing
|
|
1438
|
+
* off to once a second while it does not, spending at most half of its time capturing, with at most two
|
|
1439
|
+
* captures at once across all devices. A helper that fails before its first frame gives way to the screenshot
|
|
1440
|
+
* loop.
|
|
1410
1441
|
*/
|
|
1411
1442
|
var FramePool = class {
|
|
1412
1443
|
sources = /* @__PURE__ */ new Map();
|
|
@@ -1422,7 +1453,7 @@ var FramePool = class {
|
|
|
1422
1453
|
}
|
|
1423
1454
|
subscribe(device, listener, hint = DEFAULT_FRAME_HINT) {
|
|
1424
1455
|
const helper = this.helper();
|
|
1425
|
-
if (helper === null
|
|
1456
|
+
if (helper === null) return this.screenshots(device).add(listener);
|
|
1426
1457
|
let streamed = false;
|
|
1427
1458
|
let cancelled = false;
|
|
1428
1459
|
const { video } = listener;
|
|
@@ -1490,9 +1521,15 @@ var FramePool = class {
|
|
|
1490
1521
|
const key = `helper:${deviceKey(device)}`;
|
|
1491
1522
|
const existing = this.sources.get(key);
|
|
1492
1523
|
if (existing instanceof HelperSource) return existing;
|
|
1524
|
+
const lit = device.platform === "ios" && device.foldable ? (display) => {
|
|
1525
|
+
const panel = DUO_PANELS[display];
|
|
1526
|
+
if (!panel) return void 0;
|
|
1527
|
+
this.litPanels.set(device.udid, panel);
|
|
1528
|
+
return POSTURES[panel];
|
|
1529
|
+
} : void 0;
|
|
1493
1530
|
const created = new HelperSource(helper, device, this.env, () => {
|
|
1494
1531
|
if (this.sources.get(key) === created) this.sources.delete(key);
|
|
1495
|
-
});
|
|
1532
|
+
}, this.limits.lingerMs, lit);
|
|
1496
1533
|
this.sources.set(key, created);
|
|
1497
1534
|
return created;
|
|
1498
1535
|
}
|
|
@@ -1696,8 +1733,6 @@ function adbPath(env) {
|
|
|
1696
1733
|
return "adb";
|
|
1697
1734
|
}
|
|
1698
1735
|
const ADB_TIMEOUT_MS = 1e4;
|
|
1699
|
-
/** `sim-fold` waits up to 20 seconds for SpringBoard to finish the fold; Stim Desktop stops it after 40. */
|
|
1700
|
-
const FOLD_TIMEOUT_MS = 4e4;
|
|
1701
1736
|
const POSTURE_TIMEOUT_MS = 5e3;
|
|
1702
1737
|
function runQuietly(env, file, args, label, timeoutMs) {
|
|
1703
1738
|
return new Promise((resolve, reject) => {
|
|
@@ -1710,18 +1745,24 @@ function runQuietly(env, file, args, label, timeoutMs) {
|
|
|
1710
1745
|
]
|
|
1711
1746
|
});
|
|
1712
1747
|
let stderr = "";
|
|
1713
|
-
const
|
|
1748
|
+
const finish = (code) => {
|
|
1749
|
+
clearTimeout(timer);
|
|
1750
|
+
if (code === 0) resolve();
|
|
1751
|
+
else reject(/* @__PURE__ */ new Error(`${label} failed (code ${code}): ${stderr.trim()}`));
|
|
1752
|
+
};
|
|
1753
|
+
const timer = setTimeout(() => {
|
|
1754
|
+
child.stderr.destroy();
|
|
1755
|
+
if (child.exitCode !== null || child.signalCode !== null) return finish(child.exitCode);
|
|
1756
|
+
terminate(child);
|
|
1757
|
+
reject(/* @__PURE__ */ new Error(`${label} did not finish within ${timeoutMs / 1e3} s.`));
|
|
1758
|
+
}, timeoutMs);
|
|
1714
1759
|
child.stderr.setEncoding("utf8");
|
|
1715
1760
|
child.stderr.on("data", (chunk) => stderr = (stderr + chunk).slice(-500));
|
|
1716
1761
|
child.on("error", (error) => {
|
|
1717
1762
|
clearTimeout(timer);
|
|
1718
1763
|
reject(/* @__PURE__ */ new Error(`${label} could not start (${error.message}).`));
|
|
1719
1764
|
});
|
|
1720
|
-
child.on("close",
|
|
1721
|
-
clearTimeout(timer);
|
|
1722
|
-
if (code === 0) resolve();
|
|
1723
|
-
else reject(/* @__PURE__ */ new Error(`${label} failed (code ${code}): ${stderr.trim()}`));
|
|
1724
|
-
});
|
|
1765
|
+
child.on("close", finish);
|
|
1725
1766
|
});
|
|
1726
1767
|
}
|
|
1727
1768
|
function runAdb(env, serial, args) {
|
|
@@ -1939,7 +1980,7 @@ var ControlHub = class {
|
|
|
1939
1980
|
"spawn",
|
|
1940
1981
|
udid,
|
|
1941
1982
|
helper
|
|
1942
|
-
], "sim-fold",
|
|
1983
|
+
], "sim-fold", this.options.foldTimeoutMs);
|
|
1943
1984
|
this.options.frames.folded(udid, posture === "folded" ? "folded" : "unfolded");
|
|
1944
1985
|
return null;
|
|
1945
1986
|
} catch (cause) {
|
|
@@ -2101,9 +2142,12 @@ var Feed = class {
|
|
|
2101
2142
|
return;
|
|
2102
2143
|
}
|
|
2103
2144
|
if (!isJsonObject(value)) return;
|
|
2104
|
-
this.kept.push(
|
|
2145
|
+
this.kept.push({
|
|
2146
|
+
value,
|
|
2147
|
+
text: line
|
|
2148
|
+
});
|
|
2105
2149
|
if (this.kept.length > spec.keep) this.kept.shift();
|
|
2106
|
-
for (const listener of this.listeners) listener.item(value);
|
|
2150
|
+
for (const listener of this.listeners) listener.item(value, line);
|
|
2107
2151
|
});
|
|
2108
2152
|
const exited = (reason) => {
|
|
2109
2153
|
if (this.child !== child) return;
|
|
@@ -2119,7 +2163,7 @@ var Feed = class {
|
|
|
2119
2163
|
}
|
|
2120
2164
|
add(listener) {
|
|
2121
2165
|
this.listeners.add(listener);
|
|
2122
|
-
for (const value of this.kept) listener.item(value);
|
|
2166
|
+
for (const { value, text } of this.kept) listener.item(value, text);
|
|
2123
2167
|
return () => {
|
|
2124
2168
|
if (this.listeners.delete(listener) && this.listeners.size === 0) this.stop();
|
|
2125
2169
|
};
|
|
@@ -2673,7 +2717,8 @@ const CONTROL_LIMITS = {
|
|
|
2673
2717
|
leaseFor: "2m",
|
|
2674
2718
|
inputPerSecond: 120,
|
|
2675
2719
|
textCharsPerSecond: 40,
|
|
2676
|
-
shapeChangesPerSecond: 2
|
|
2720
|
+
shapeChangesPerSecond: 2,
|
|
2721
|
+
foldTimeoutMs: 4e4
|
|
2677
2722
|
};
|
|
2678
2723
|
const LOCK_LIMITS = {
|
|
2679
2724
|
timeoutMs: 3e4,
|
|
@@ -2768,7 +2813,7 @@ function take(bucket, cost, perSecond, capacity) {
|
|
|
2768
2813
|
return true;
|
|
2769
2814
|
}
|
|
2770
2815
|
function send(socket, message) {
|
|
2771
|
-
if (socket.readyState === socket.OPEN) socket.send(JSON.stringify(message));
|
|
2816
|
+
if (socket.readyState === socket.OPEN) socket.send(typeof message === "string" ? message : JSON.stringify(message));
|
|
2772
2817
|
}
|
|
2773
2818
|
function requestId(value) {
|
|
2774
2819
|
return typeof value === "number" || typeof value === "string" ? value : null;
|
|
@@ -2871,7 +2916,8 @@ async function startServer(options) {
|
|
|
2871
2916
|
idleMs: controlLimits.idleMs,
|
|
2872
2917
|
renewMs: controlLimits.renewMs,
|
|
2873
2918
|
leaseFor: controlLimits.leaseFor,
|
|
2874
|
-
foldHelper
|
|
2919
|
+
foldHelper,
|
|
2920
|
+
foldTimeoutMs: controlLimits.foldTimeoutMs
|
|
2875
2921
|
});
|
|
2876
2922
|
const wss = new WebSocketServer({
|
|
2877
2923
|
noServer: true,
|
|
@@ -3078,12 +3124,9 @@ async function startServer(options) {
|
|
|
3078
3124
|
function subscribeStatus(id) {
|
|
3079
3125
|
const subscription = openSubscription(id);
|
|
3080
3126
|
if (!subscription) return;
|
|
3127
|
+
const envelope = `{"event":"status","subscription":${JSON.stringify(subscription)},"payload":`;
|
|
3081
3128
|
const unsubscribe = feeds.subscribe(STATUS_FEED, {
|
|
3082
|
-
item: (
|
|
3083
|
-
event: "status",
|
|
3084
|
-
subscription,
|
|
3085
|
-
payload
|
|
3086
|
-
}),
|
|
3129
|
+
item: (_payload, text) => send(socket, `${envelope}${text}}`),
|
|
3087
3130
|
failed: (message) => {
|
|
3088
3131
|
subscriptions.delete(subscription);
|
|
3089
3132
|
send(socket, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stim-cli/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.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.13.0",
|
|
32
|
+
"stim": "1.13.0",
|
|
33
33
|
"ws": "^8.21.3"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|