@mcesystems/usbmuxd-instance-manager 1.0.72
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 +245 -0
- package/dist/cli.js +750 -0
- package/dist/cli.js.map +7 -0
- package/dist/cli.mjs +727 -0
- package/dist/cli.mjs.map +7 -0
- package/dist/index.js +680 -0
- package/dist/index.js.map +7 -0
- package/dist/index.mjs +642 -0
- package/dist/index.mjs.map +7 -0
- package/dist/types/InstanceManager.d.ts +114 -0
- package/dist/types/InstanceManager.d.ts.map +1 -0
- package/dist/types/UsbmuxdService.d.ts +59 -0
- package/dist/types/UsbmuxdService.d.ts.map +1 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +125 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/package.json +61 -0
- package/prebuilt/alpine-usbmuxd.tar.gz +0 -0
- package/scripts/README.md +191 -0
- package/scripts/attach-device.ps1 +192 -0
- package/scripts/install-windows.ps1 +254 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
InstanceManager: () => InstanceManager,
|
|
34
|
+
UsbmuxdService: () => UsbmuxdService
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// src/UsbmuxdService.ts
|
|
39
|
+
var import_node_events2 = require("node:events");
|
|
40
|
+
var import_tool_debug_g42 = require("@mcesystems/tool-debug-g4");
|
|
41
|
+
var import_usb_device_listener = __toESM(require("@mcesystems/usb-device-listener"));
|
|
42
|
+
|
|
43
|
+
// src/InstanceManager.ts
|
|
44
|
+
var import_node_child_process = require("node:child_process");
|
|
45
|
+
var import_node_events = require("node:events");
|
|
46
|
+
var import_node_util = require("node:util");
|
|
47
|
+
var import_tool_debug_g4 = require("@mcesystems/tool-debug-g4");
|
|
48
|
+
var { logInfo, logWarning } = (0, import_tool_debug_g4.createLoggers)("usbmuxd-instance-manager");
|
|
49
|
+
var execAsync = (0, import_node_util.promisify)(import_node_child_process.exec);
|
|
50
|
+
var USBIPD_PATH = '"C:\\Program Files\\usbipd-win\\usbipd.exe"';
|
|
51
|
+
function parseUsbipdList(output) {
|
|
52
|
+
const devices = [];
|
|
53
|
+
const lines = output.split(/\r?\n/);
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
const match = line.match(/^(\d+-\d+)\s+([0-9a-f]{4}):([0-9a-f]{4})\s+(.+)$/i);
|
|
56
|
+
if (match) {
|
|
57
|
+
const rest = match[4].trim();
|
|
58
|
+
const stateMatch = rest.match(/^(.+?)\s{2,}(\S.*)$/);
|
|
59
|
+
const description = stateMatch ? stateMatch[1].trim() : rest;
|
|
60
|
+
const state = stateMatch ? stateMatch[2].trim() : "Unknown";
|
|
61
|
+
devices.push({
|
|
62
|
+
busId: match[1],
|
|
63
|
+
vid: match[2].toUpperCase(),
|
|
64
|
+
pid: match[3].toUpperCase(),
|
|
65
|
+
description,
|
|
66
|
+
state
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return devices;
|
|
71
|
+
}
|
|
72
|
+
var DEFAULT_CONFIG = {
|
|
73
|
+
batchSize: 4,
|
|
74
|
+
basePort: 27015,
|
|
75
|
+
maxInstances: 20,
|
|
76
|
+
usbmuxdPath: "usbmuxd",
|
|
77
|
+
// Path inside WSL2
|
|
78
|
+
wslDistribution: "alpine-usbmuxd-build",
|
|
79
|
+
// Alpine WSL2 distribution name
|
|
80
|
+
verboseLogging: true,
|
|
81
|
+
appleVendorId: "05AC"
|
|
82
|
+
};
|
|
83
|
+
var InstanceManager = class extends import_node_events.EventEmitter {
|
|
84
|
+
config;
|
|
85
|
+
instances = /* @__PURE__ */ new Map();
|
|
86
|
+
deviceMappings = /* @__PURE__ */ new Map();
|
|
87
|
+
processes = /* @__PURE__ */ new Map();
|
|
88
|
+
nextInstanceId = 1;
|
|
89
|
+
startedAt = null;
|
|
90
|
+
isRunning = false;
|
|
91
|
+
/** Tracks which devices have been attached to WSL */
|
|
92
|
+
attachedDevices = /* @__PURE__ */ new Set();
|
|
93
|
+
/** Cached WSL IP address for connecting from Windows */
|
|
94
|
+
wslIpAddress = null;
|
|
95
|
+
constructor(config = {}) {
|
|
96
|
+
super();
|
|
97
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Detect the WSL2 IP address for the configured distribution
|
|
101
|
+
* This IP is needed to connect from Windows to services inside WSL
|
|
102
|
+
*/
|
|
103
|
+
async detectWslIpAddress() {
|
|
104
|
+
if (this.wslIpAddress) {
|
|
105
|
+
return this.wslIpAddress;
|
|
106
|
+
}
|
|
107
|
+
const distro = this.config.wslDistribution || "alpine-usbmuxd-build";
|
|
108
|
+
try {
|
|
109
|
+
const { stdout } = await execAsync(`wsl -d ${distro} -- ip -4 addr show eth0`);
|
|
110
|
+
const match = stdout.match(/inet\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
111
|
+
if (match) {
|
|
112
|
+
this.wslIpAddress = match[1];
|
|
113
|
+
logInfo(`Detected WSL IP address: ${this.wslIpAddress}`);
|
|
114
|
+
return this.wslIpAddress;
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
logWarning(`Failed to detect WSL IP via ip addr: ${error}`);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const { stdout } = await execAsync(`wsl -d ${distro} -- hostname -I`);
|
|
121
|
+
const ip = stdout.trim().split(/\s+/)[0];
|
|
122
|
+
if (ip && /^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
|
|
123
|
+
this.wslIpAddress = ip;
|
|
124
|
+
logInfo(`Detected WSL IP address (hostname): ${this.wslIpAddress}`);
|
|
125
|
+
return this.wslIpAddress;
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
logWarning(`Failed to detect WSL IP via hostname: ${error}`);
|
|
129
|
+
}
|
|
130
|
+
logWarning("Could not detect WSL IP, falling back to localhost");
|
|
131
|
+
this.wslIpAddress = "127.0.0.1";
|
|
132
|
+
return this.wslIpAddress;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Start the instance manager
|
|
136
|
+
*/
|
|
137
|
+
start() {
|
|
138
|
+
if (this.isRunning) {
|
|
139
|
+
throw new Error("Instance manager is already running");
|
|
140
|
+
}
|
|
141
|
+
this.isRunning = true;
|
|
142
|
+
this.startedAt = /* @__PURE__ */ new Date();
|
|
143
|
+
this.emit("started");
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Stop the instance manager and all instances
|
|
147
|
+
*/
|
|
148
|
+
async stop() {
|
|
149
|
+
if (!this.isRunning) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.isRunning = false;
|
|
153
|
+
const detachPromises = Array.from(this.attachedDevices).map(
|
|
154
|
+
(busId) => this.detachDeviceFromWsl(busId)
|
|
155
|
+
);
|
|
156
|
+
await Promise.all(detachPromises);
|
|
157
|
+
this.attachedDevices.clear();
|
|
158
|
+
const stopPromises = Array.from(this.instances.keys()).map((id) => this.stopInstance(id));
|
|
159
|
+
await Promise.all(stopPromises);
|
|
160
|
+
this.instances.clear();
|
|
161
|
+
this.deviceMappings.clear();
|
|
162
|
+
this.processes.clear();
|
|
163
|
+
this.emit("stopped");
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Find the usbipd bus ID for a device by matching VID/PID
|
|
167
|
+
*/
|
|
168
|
+
async findBusIdForDevice(device) {
|
|
169
|
+
try {
|
|
170
|
+
const { stdout } = await execAsync(`${USBIPD_PATH} list`);
|
|
171
|
+
const usbipdDevices = parseUsbipdList(stdout);
|
|
172
|
+
const deviceVid = device.vid.toString(16).toUpperCase().padStart(4, "0");
|
|
173
|
+
const devicePid = device.pid.toString(16).toUpperCase().padStart(4, "0");
|
|
174
|
+
logInfo(`Looking for device with VID:PID ${deviceVid}:${devicePid}`);
|
|
175
|
+
logInfo(`Found ${usbipdDevices.length} devices from usbipd list`);
|
|
176
|
+
const match = usbipdDevices.find((d) => d.vid === deviceVid && d.pid === devicePid);
|
|
177
|
+
if (match) {
|
|
178
|
+
logInfo(
|
|
179
|
+
`Found usbipd bus ID ${match.busId} for device ${device.deviceId} (${deviceVid}:${devicePid})`
|
|
180
|
+
);
|
|
181
|
+
return match.busId;
|
|
182
|
+
}
|
|
183
|
+
for (const d of usbipdDevices) {
|
|
184
|
+
logInfo(` usbipd device: ${d.busId} ${d.vid}:${d.pid} "${d.description}"`);
|
|
185
|
+
}
|
|
186
|
+
logWarning(
|
|
187
|
+
`Could not find usbipd bus ID for device ${device.deviceId} (${deviceVid}:${devicePid})`
|
|
188
|
+
);
|
|
189
|
+
return null;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
logWarning(`Failed to run usbipd list: ${error}`);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Ensure the WSL distribution is running
|
|
197
|
+
*/
|
|
198
|
+
async ensureWslRunning() {
|
|
199
|
+
const distro = this.config.wslDistribution;
|
|
200
|
+
try {
|
|
201
|
+
if (distro) {
|
|
202
|
+
logInfo(`Starting WSL distribution: ${distro}...`);
|
|
203
|
+
await execAsync(`wsl -d ${distro} -- echo "WSL started"`);
|
|
204
|
+
} else {
|
|
205
|
+
logInfo("Starting default WSL distribution...");
|
|
206
|
+
await execAsync(`wsl -- echo "WSL started"`);
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
logWarning(`Failed to start WSL: ${error}`);
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Attach a device to WSL via usbipd
|
|
216
|
+
* Note: This requires administrator privileges
|
|
217
|
+
*/
|
|
218
|
+
async attachDeviceToWsl(busId) {
|
|
219
|
+
const distro = this.config.wslDistribution;
|
|
220
|
+
try {
|
|
221
|
+
await this.ensureWslRunning();
|
|
222
|
+
logInfo(`Binding device ${busId}...`);
|
|
223
|
+
await execAsync(`${USBIPD_PATH} bind --busid ${busId} --force`);
|
|
224
|
+
if (distro) {
|
|
225
|
+
logInfo(`Attaching device ${busId} to WSL distribution ${distro}...`);
|
|
226
|
+
await execAsync(`${USBIPD_PATH} attach --wsl=${distro} --busid=${busId}`);
|
|
227
|
+
} else {
|
|
228
|
+
logInfo(`Attaching device ${busId} to default WSL...`);
|
|
229
|
+
await execAsync(`${USBIPD_PATH} attach --wsl --busid=${busId}`);
|
|
230
|
+
}
|
|
231
|
+
logInfo(`Device ${busId} attached to WSL successfully`);
|
|
232
|
+
return true;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
logWarning(`Failed to attach device ${busId} to WSL: ${error}`);
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Detach a device from WSL via usbipd
|
|
240
|
+
*/
|
|
241
|
+
async detachDeviceFromWsl(busId) {
|
|
242
|
+
try {
|
|
243
|
+
await execAsync(`${USBIPD_PATH} detach --busid=${busId}`);
|
|
244
|
+
logInfo(`Device ${busId} detached from WSL`);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
logWarning(`Failed to detach device ${busId}: ${error}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Handle device connection
|
|
251
|
+
* Attaches device to WSL, then assigns to an existing instance or creates a new one
|
|
252
|
+
*/
|
|
253
|
+
async onDeviceConnected(device) {
|
|
254
|
+
if (this.deviceMappings.has(device.deviceId)) {
|
|
255
|
+
logWarning(`Device ${device.deviceId} is already connected`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const busId = await this.findBusIdForDevice(device);
|
|
259
|
+
if (!busId) {
|
|
260
|
+
logWarning(`Cannot attach device ${device.deviceId} - bus ID not found`);
|
|
261
|
+
}
|
|
262
|
+
if (busId && !this.attachedDevices.has(busId)) {
|
|
263
|
+
const attached = await this.attachDeviceToWsl(busId);
|
|
264
|
+
if (attached) {
|
|
265
|
+
this.attachedDevices.add(busId);
|
|
266
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
let targetInstance = this.findInstanceWithCapacity();
|
|
270
|
+
if (!targetInstance) {
|
|
271
|
+
if (this.instances.size >= this.config.maxInstances) {
|
|
272
|
+
throw new Error(`Maximum number of instances (${this.config.maxInstances}) reached`);
|
|
273
|
+
}
|
|
274
|
+
targetInstance = await this.createInstance();
|
|
275
|
+
}
|
|
276
|
+
targetInstance.deviceUdids.push(device.deviceId);
|
|
277
|
+
const mapping = {
|
|
278
|
+
udid: device.deviceId,
|
|
279
|
+
instanceId: targetInstance.id,
|
|
280
|
+
host: targetInstance.host,
|
|
281
|
+
port: targetInstance.port,
|
|
282
|
+
addedAt: /* @__PURE__ */ new Date(),
|
|
283
|
+
busId: busId ?? void 0
|
|
284
|
+
};
|
|
285
|
+
this.deviceMappings.set(device.deviceId, mapping);
|
|
286
|
+
this.emit("device-assigned", {
|
|
287
|
+
device,
|
|
288
|
+
instance: targetInstance,
|
|
289
|
+
mapping
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Pair a device with the usbmuxd host.
|
|
294
|
+
* This is required once per device before most commands will work.
|
|
295
|
+
* The pairing record is stored in WSL and persists across restarts.
|
|
296
|
+
*
|
|
297
|
+
* @param udid Device UDID to pair
|
|
298
|
+
* @param goIosPath Optional path to go-ios binary (defaults to "ios")
|
|
299
|
+
* @returns true if pairing succeeded, false otherwise
|
|
300
|
+
*/
|
|
301
|
+
async pairDevice(udid, goIosPath = "ios") {
|
|
302
|
+
const mapping = this.deviceMappings.get(udid);
|
|
303
|
+
if (!mapping) {
|
|
304
|
+
logWarning(`Cannot pair device ${udid} - not found in mappings`);
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
const socketAddress = `${mapping.host}:${mapping.port}`;
|
|
309
|
+
logInfo(`Pairing device ${udid} via ${socketAddress}...`);
|
|
310
|
+
const { stderr } = await execAsync(`"${goIosPath}" pair --udid=${udid}`, {
|
|
311
|
+
env: { ...process.env, USBMUXD_SOCKET_ADDRESS: socketAddress }
|
|
312
|
+
});
|
|
313
|
+
if (stderr?.includes("error")) {
|
|
314
|
+
logWarning(`Pairing warning for ${udid}: ${stderr}`);
|
|
315
|
+
}
|
|
316
|
+
logInfo(`Device ${udid} paired successfully`);
|
|
317
|
+
this.emit("device-paired", { udid, mapping });
|
|
318
|
+
return true;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
logWarning(`Failed to pair device ${udid}: ${error}`);
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Handle device disconnection
|
|
326
|
+
* Detaches from WSL, removes device from instance, and stops instance if empty
|
|
327
|
+
*/
|
|
328
|
+
async onDeviceDisconnected(device) {
|
|
329
|
+
const mapping = this.deviceMappings.get(device.deviceId);
|
|
330
|
+
if (!mapping) {
|
|
331
|
+
logWarning(`Device ${device.deviceId} was not tracked`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (mapping.busId) {
|
|
335
|
+
await this.detachDeviceFromWsl(mapping.busId);
|
|
336
|
+
this.attachedDevices.delete(mapping.busId);
|
|
337
|
+
}
|
|
338
|
+
const instance = this.instances.get(mapping.instanceId);
|
|
339
|
+
if (!instance) {
|
|
340
|
+
logWarning(`Instance ${mapping.instanceId} not found`);
|
|
341
|
+
this.deviceMappings.delete(device.deviceId);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const deviceIndex = instance.deviceUdids.indexOf(device.deviceId);
|
|
345
|
+
if (deviceIndex > -1) {
|
|
346
|
+
instance.deviceUdids.splice(deviceIndex, 1);
|
|
347
|
+
}
|
|
348
|
+
this.deviceMappings.delete(device.deviceId);
|
|
349
|
+
this.emit("device-removed", {
|
|
350
|
+
device,
|
|
351
|
+
instance
|
|
352
|
+
});
|
|
353
|
+
if (instance.deviceUdids.length === 0) {
|
|
354
|
+
await this.stopInstance(instance.id);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Find an instance with available capacity
|
|
359
|
+
*/
|
|
360
|
+
findInstanceWithCapacity() {
|
|
361
|
+
for (const instance of this.instances.values()) {
|
|
362
|
+
if (instance.deviceUdids.length < this.config.batchSize) {
|
|
363
|
+
return instance;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Create a new usbmuxd instance
|
|
370
|
+
*/
|
|
371
|
+
async createInstance() {
|
|
372
|
+
const instanceId = this.nextInstanceId++;
|
|
373
|
+
const port = this.config.basePort + instanceId - 1;
|
|
374
|
+
const host = await this.detectWslIpAddress();
|
|
375
|
+
const usbmuxdArgs = [
|
|
376
|
+
"-f",
|
|
377
|
+
// Foreground
|
|
378
|
+
"-v",
|
|
379
|
+
// Verbose (if enabled)
|
|
380
|
+
"-S",
|
|
381
|
+
`0.0.0.0:${port}`,
|
|
382
|
+
// Listen on all interfaces (for Windows → WSL2)
|
|
383
|
+
"--pidfile",
|
|
384
|
+
"NONE"
|
|
385
|
+
];
|
|
386
|
+
if (!this.config.verboseLogging) {
|
|
387
|
+
usbmuxdArgs.splice(1, 1);
|
|
388
|
+
}
|
|
389
|
+
const wslArgs = [
|
|
390
|
+
"-d",
|
|
391
|
+
this.config.wslDistribution || "alpine-usbmuxd-build",
|
|
392
|
+
this.config.usbmuxdPath,
|
|
393
|
+
...usbmuxdArgs
|
|
394
|
+
];
|
|
395
|
+
const process2 = (0, import_node_child_process.spawn)("wsl", wslArgs, {
|
|
396
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
397
|
+
windowsHide: false
|
|
398
|
+
// Show console for debugging
|
|
399
|
+
});
|
|
400
|
+
process2.stdout?.on("data", (data) => {
|
|
401
|
+
this.emit("instance-log", {
|
|
402
|
+
instanceId,
|
|
403
|
+
level: "info",
|
|
404
|
+
message: data.toString().trim()
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
process2.stderr?.on("data", (data) => {
|
|
408
|
+
this.emit("instance-log", {
|
|
409
|
+
instanceId,
|
|
410
|
+
level: "error",
|
|
411
|
+
message: data.toString().trim()
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
process2.on("exit", (code, signal) => {
|
|
415
|
+
this.emit("instance-exited", {
|
|
416
|
+
instanceId,
|
|
417
|
+
code,
|
|
418
|
+
signal
|
|
419
|
+
});
|
|
420
|
+
if (this.instances.has(instanceId)) {
|
|
421
|
+
this.instances.delete(instanceId);
|
|
422
|
+
this.processes.delete(instanceId);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
const pid = process2.pid;
|
|
426
|
+
if (pid === void 0) {
|
|
427
|
+
process2.kill("SIGKILL");
|
|
428
|
+
throw new Error("Failed to get PID for usbmuxd instance");
|
|
429
|
+
}
|
|
430
|
+
const instance = {
|
|
431
|
+
id: instanceId,
|
|
432
|
+
host,
|
|
433
|
+
port,
|
|
434
|
+
pid,
|
|
435
|
+
deviceUdids: [],
|
|
436
|
+
startedAt: /* @__PURE__ */ new Date()
|
|
437
|
+
};
|
|
438
|
+
this.instances.set(instanceId, instance);
|
|
439
|
+
this.processes.set(instanceId, process2);
|
|
440
|
+
this.emit("instance-started", instance);
|
|
441
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
442
|
+
return instance;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Stop a specific instance
|
|
446
|
+
*/
|
|
447
|
+
async stopInstance(instanceId) {
|
|
448
|
+
const instance = this.instances.get(instanceId);
|
|
449
|
+
const process2 = this.processes.get(instanceId);
|
|
450
|
+
if (!instance || !process2) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
process2.kill("SIGTERM");
|
|
454
|
+
await new Promise((resolve) => {
|
|
455
|
+
const timeout = setTimeout(() => {
|
|
456
|
+
if (!process2.killed) {
|
|
457
|
+
process2.kill("SIGKILL");
|
|
458
|
+
}
|
|
459
|
+
resolve();
|
|
460
|
+
}, 5e3);
|
|
461
|
+
process2.once("exit", () => {
|
|
462
|
+
clearTimeout(timeout);
|
|
463
|
+
resolve();
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
this.instances.delete(instanceId);
|
|
467
|
+
this.processes.delete(instanceId);
|
|
468
|
+
for (const [udid, mapping] of this.deviceMappings.entries()) {
|
|
469
|
+
if (mapping.instanceId === instanceId) {
|
|
470
|
+
this.deviceMappings.delete(udid);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
this.emit("instance-stopped", instance);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Get device-to-port mapping for a specific UDID
|
|
477
|
+
*/
|
|
478
|
+
getDevicePort(udid) {
|
|
479
|
+
const mapping = this.deviceMappings.get(udid);
|
|
480
|
+
return mapping ? mapping.port : null;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Get all device mappings
|
|
484
|
+
*/
|
|
485
|
+
getDeviceMappings() {
|
|
486
|
+
return Array.from(this.deviceMappings.values());
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Get all running instances
|
|
490
|
+
*/
|
|
491
|
+
getInstances() {
|
|
492
|
+
return Array.from(this.instances.values());
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Get manager statistics
|
|
496
|
+
*/
|
|
497
|
+
getStats() {
|
|
498
|
+
const now = Date.now();
|
|
499
|
+
const startTime = this.startedAt?.getTime() || now;
|
|
500
|
+
const uptimeSeconds = Math.floor((now - startTime) / 1e3);
|
|
501
|
+
return {
|
|
502
|
+
instanceCount: this.instances.size,
|
|
503
|
+
deviceCount: this.deviceMappings.size,
|
|
504
|
+
uptimeSeconds,
|
|
505
|
+
startedAt: this.startedAt || /* @__PURE__ */ new Date()
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Get current configuration
|
|
510
|
+
*/
|
|
511
|
+
getConfig() {
|
|
512
|
+
return { ...this.config };
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/UsbmuxdService.ts
|
|
517
|
+
var { logInfo: logInfo2, logError } = (0, import_tool_debug_g42.createLoggers)("usbmuxd-instance-manager");
|
|
518
|
+
var UsbmuxdService = class extends import_node_events2.EventEmitter {
|
|
519
|
+
manager;
|
|
520
|
+
usbListener;
|
|
521
|
+
isListening = false;
|
|
522
|
+
constructor(config = {}) {
|
|
523
|
+
super();
|
|
524
|
+
this.manager = new InstanceManager(config);
|
|
525
|
+
this.usbListener = new import_usb_device_listener.default();
|
|
526
|
+
this.setupEventHandlers();
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Forward manager events to service subscribers (no logging; CLI/examples handle logging).
|
|
530
|
+
*/
|
|
531
|
+
setupEventHandlers() {
|
|
532
|
+
this.manager.on("instance-started", (instance) => {
|
|
533
|
+
this.emit("instance-started", instance);
|
|
534
|
+
});
|
|
535
|
+
this.manager.on("instance-stopped", (instance) => {
|
|
536
|
+
this.emit("instance-stopped", instance);
|
|
537
|
+
});
|
|
538
|
+
this.manager.on(
|
|
539
|
+
"device-assigned",
|
|
540
|
+
(payload) => {
|
|
541
|
+
this.emit("device-assigned", payload);
|
|
542
|
+
}
|
|
543
|
+
);
|
|
544
|
+
this.manager.on(
|
|
545
|
+
"device-removed",
|
|
546
|
+
(payload) => {
|
|
547
|
+
this.emit("device-removed", payload);
|
|
548
|
+
}
|
|
549
|
+
);
|
|
550
|
+
this.manager.on(
|
|
551
|
+
"instance-log",
|
|
552
|
+
(payload) => {
|
|
553
|
+
this.emit("instance-log", payload);
|
|
554
|
+
}
|
|
555
|
+
);
|
|
556
|
+
this.manager.on(
|
|
557
|
+
"instance-exited",
|
|
558
|
+
(payload) => {
|
|
559
|
+
this.emit("instance-exited", payload);
|
|
560
|
+
}
|
|
561
|
+
);
|
|
562
|
+
this.manager.on("device-paired", (payload) => {
|
|
563
|
+
this.emit("device-paired", payload);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Start the service
|
|
568
|
+
* Begins monitoring for iOS devices
|
|
569
|
+
*/
|
|
570
|
+
start() {
|
|
571
|
+
if (this.isListening) {
|
|
572
|
+
throw new Error("Service is already running");
|
|
573
|
+
}
|
|
574
|
+
const config = this.manager.getConfig();
|
|
575
|
+
const appleVid = Number.parseInt(config.appleVendorId, 16);
|
|
576
|
+
logInfo2("Starting service...");
|
|
577
|
+
logInfo2(`Batch size: ${config.batchSize} devices per instance`);
|
|
578
|
+
logInfo2(`Base port: ${config.basePort}`);
|
|
579
|
+
logInfo2(`Max instances: ${config.maxInstances}`);
|
|
580
|
+
logInfo2(`usbmuxd path: ${config.usbmuxdPath}`);
|
|
581
|
+
logInfo2(`Monitoring Apple devices (VID: ${config.appleVendorId})`);
|
|
582
|
+
this.usbListener.onDeviceAdd(async (device) => {
|
|
583
|
+
if (device.vid !== appleVid) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
logInfo2(`Apple device connected: ${device.deviceId} (${device.deviceName || "Unknown"})`);
|
|
587
|
+
try {
|
|
588
|
+
await this.manager.onDeviceConnected(device);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
logError("Error handling device connection:", error);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
this.usbListener.onDeviceRemove(async (device) => {
|
|
594
|
+
if (device.vid !== appleVid) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
logInfo2(`Apple device disconnected: ${device.deviceId}`);
|
|
598
|
+
try {
|
|
599
|
+
await this.manager.onDeviceDisconnected(device);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
logError("Error handling device disconnection:", error);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
try {
|
|
605
|
+
this.usbListener.startListening({
|
|
606
|
+
targetDevices: []
|
|
607
|
+
// Monitor all devices, we'll filter by VID
|
|
608
|
+
});
|
|
609
|
+
this.isListening = true;
|
|
610
|
+
this.manager.start();
|
|
611
|
+
logInfo2("Service started successfully");
|
|
612
|
+
logInfo2("Waiting for iOS devices...");
|
|
613
|
+
} catch (error) {
|
|
614
|
+
logError("Failed to start USB listener:", error);
|
|
615
|
+
throw error;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Stop the service
|
|
620
|
+
* Stops monitoring and terminates all instances
|
|
621
|
+
*/
|
|
622
|
+
async stop() {
|
|
623
|
+
if (!this.isListening) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
logInfo2("Stopping service...");
|
|
627
|
+
this.usbListener.stopListening();
|
|
628
|
+
this.isListening = false;
|
|
629
|
+
await this.manager.stop();
|
|
630
|
+
logInfo2("Service stopped");
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Get device port mapping
|
|
634
|
+
*/
|
|
635
|
+
getDevicePort(udid) {
|
|
636
|
+
return this.manager.getDevicePort(udid);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Get all device mappings
|
|
640
|
+
*/
|
|
641
|
+
getDeviceMappings() {
|
|
642
|
+
return this.manager.getDeviceMappings();
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Get all running instances
|
|
646
|
+
*/
|
|
647
|
+
getInstances() {
|
|
648
|
+
return this.manager.getInstances();
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Get service statistics
|
|
652
|
+
*/
|
|
653
|
+
getStats() {
|
|
654
|
+
return this.manager.getStats();
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Get current configuration
|
|
658
|
+
*/
|
|
659
|
+
getConfig() {
|
|
660
|
+
return this.manager.getConfig();
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Pair a device with the usbmuxd host.
|
|
664
|
+
* This is required once per device before most commands will work.
|
|
665
|
+
* The pairing record is stored in WSL and persists across restarts.
|
|
666
|
+
*
|
|
667
|
+
* @param udid Device UDID to pair
|
|
668
|
+
* @param goIosPath Optional path to go-ios binary
|
|
669
|
+
* @returns true if pairing succeeded, false otherwise
|
|
670
|
+
*/
|
|
671
|
+
async pairDevice(udid, goIosPath) {
|
|
672
|
+
return this.manager.pairDevice(udid, goIosPath);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
676
|
+
0 && (module.exports = {
|
|
677
|
+
InstanceManager,
|
|
678
|
+
UsbmuxdService
|
|
679
|
+
});
|
|
680
|
+
//# sourceMappingURL=index.js.map
|