@mcesystems/usbmuxd-instance-manager 1.0.72 → 1.0.73

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/dist/index.js CHANGED
@@ -37,16 +37,100 @@ module.exports = __toCommonJS(index_exports);
37
37
 
38
38
  // src/UsbmuxdService.ts
39
39
  var import_node_events2 = require("node:events");
40
- var import_tool_debug_g42 = require("@mcesystems/tool-debug-g4");
40
+ var import_tool_debug_g43 = require("@mcesystems/tool-debug-g4");
41
41
  var import_usb_device_listener = __toESM(require("@mcesystems/usb-device-listener"));
42
42
 
43
43
  // src/InstanceManager.ts
44
- var import_node_child_process = require("node:child_process");
44
+ var import_node_child_process2 = require("node:child_process");
45
45
  var import_node_events = require("node:events");
46
+ var import_node_util2 = require("node:util");
47
+ var import_tool_debug_g42 = require("@mcesystems/tool-debug-g4");
48
+
49
+ // src/LockdownSync.ts
50
+ var import_node_child_process = require("node:child_process");
51
+ var import_node_fs = require("node:fs");
52
+ var import_node_path = require("node:path");
46
53
  var import_node_util = require("node:util");
47
54
  var import_tool_debug_g4 = require("@mcesystems/tool-debug-g4");
48
55
  var { logInfo, logWarning } = (0, import_tool_debug_g4.createLoggers)("usbmuxd-instance-manager");
49
56
  var execAsync = (0, import_node_util.promisify)(import_node_child_process.exec);
57
+ var ALPINE_LOCKDOWN_DIR = "/var/lib/lockdown";
58
+ var SYSTEM_CONFIG_PLIST = "SystemConfiguration.plist";
59
+ function windowsPathToWsl(windowsPath) {
60
+ const normalized = windowsPath.replace(/\\/g, "/").trim();
61
+ const driveMatch = normalized.match(/^([a-zA-Z]):\/?(.*)$/);
62
+ if (driveMatch) {
63
+ const drive = driveMatch[1].toLowerCase();
64
+ const rest = driveMatch[2] || "";
65
+ return `/mnt/${drive}${rest ? `/${rest}` : ""}`;
66
+ }
67
+ return normalized;
68
+ }
69
+ async function syncToAlpine(udid, config) {
70
+ if (!config.lockdownSyncEnabled || !config.lockdownWindowsPath?.trim()) {
71
+ return;
72
+ }
73
+ const windowsDir = config.lockdownWindowsPath.trim();
74
+ const windowsFile = (0, import_node_path.join)(windowsDir, `${udid}.plist`);
75
+ if (!(0, import_node_fs.existsSync)(windowsFile)) {
76
+ return;
77
+ }
78
+ const distro = config.wslDistribution || "alpine-usbmuxd-build";
79
+ const wslSource = windowsPathToWsl(windowsFile);
80
+ const wslDestDir = ALPINE_LOCKDOWN_DIR;
81
+ try {
82
+ await execAsync(
83
+ `wsl -d ${distro} -- sh -c "mkdir -p ${wslDestDir} && cp '${wslSource}' '${wslDestDir}/'"`
84
+ );
85
+ logInfo(`Lockdown sync: copied ${udid}.plist to Alpine`);
86
+ } catch (error) {
87
+ try {
88
+ await execAsync(
89
+ `wsl -d ${distro} -- sh -c "mkdir -p ${wslDestDir} && sudo cp '${wslSource}' '${wslDestDir}/'"`
90
+ );
91
+ logInfo(`Lockdown sync: copied ${udid}.plist to Alpine (via sudo)`);
92
+ } catch (sudoError) {
93
+ logWarning(
94
+ `Lockdown sync to Alpine failed for ${udid}: ${error}. Sudo fallback failed: ${sudoError}. Ensure /var/lib/lockdown is writable or use passwordless sudo.`
95
+ );
96
+ }
97
+ }
98
+ }
99
+ async function syncFromAlpine(udids, config) {
100
+ if (!config.lockdownSyncEnabled || !config.lockdownWindowsPath?.trim()) {
101
+ return;
102
+ }
103
+ const windowsDir = config.lockdownWindowsPath.trim();
104
+ try {
105
+ (0, import_node_fs.mkdirSync)(windowsDir, { recursive: true });
106
+ } catch (error) {
107
+ logWarning(`Lockdown sync: could not create Windows dir ${windowsDir}: ${error}`);
108
+ return;
109
+ }
110
+ const distro = config.wslDistribution || "alpine-usbmuxd-build";
111
+ const wslDestDir = windowsPathToWsl(windowsDir);
112
+ for (const udid of udids) {
113
+ const plist = `${udid}.plist`;
114
+ try {
115
+ await execAsync(
116
+ `wsl -d ${distro} -- sh -c "test -f '${ALPINE_LOCKDOWN_DIR}/${plist}' && cp '${ALPINE_LOCKDOWN_DIR}/${plist}' '${wslDestDir}/'"`
117
+ );
118
+ logInfo(`Lockdown sync: copied ${plist} from Alpine to Windows`);
119
+ } catch {
120
+ }
121
+ }
122
+ try {
123
+ await execAsync(
124
+ `wsl -d ${distro} -- sh -c "test -f '${ALPINE_LOCKDOWN_DIR}/${SYSTEM_CONFIG_PLIST}' && cp '${ALPINE_LOCKDOWN_DIR}/${SYSTEM_CONFIG_PLIST}' '${wslDestDir}/'"`
125
+ );
126
+ logInfo(`Lockdown sync: copied ${SYSTEM_CONFIG_PLIST} from Alpine to Windows`);
127
+ } catch {
128
+ }
129
+ }
130
+
131
+ // src/InstanceManager.ts
132
+ var { logInfo: logInfo2, logWarning: logWarning2 } = (0, import_tool_debug_g42.createLoggers)("usbmuxd-instance-manager");
133
+ var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.exec);
50
134
  var USBIPD_PATH = '"C:\\Program Files\\usbipd-win\\usbipd.exe"';
51
135
  function parseUsbipdList(output) {
52
136
  const devices = [];
@@ -78,7 +162,9 @@ var DEFAULT_CONFIG = {
78
162
  wslDistribution: "alpine-usbmuxd-build",
79
163
  // Alpine WSL2 distribution name
80
164
  verboseLogging: true,
81
- appleVendorId: "05AC"
165
+ appleVendorId: "05AC",
166
+ lockdownWindowsPath: "C:\\ProgramData\\mce\\lockdown",
167
+ lockdownSyncEnabled: true
82
168
  };
83
169
  var InstanceManager = class extends import_node_events.EventEmitter {
84
170
  config;
@@ -90,6 +176,8 @@ var InstanceManager = class extends import_node_events.EventEmitter {
90
176
  isRunning = false;
91
177
  /** Tracks which devices have been attached to WSL */
92
178
  attachedDevices = /* @__PURE__ */ new Set();
179
+ /** Device IDs currently in the attach flow (ignore disconnect until attach completes) */
180
+ pendingAttachDevices = /* @__PURE__ */ new Set();
93
181
  /** Cached WSL IP address for connecting from Windows */
94
182
  wslIpAddress = null;
95
183
  constructor(config = {}) {
@@ -106,28 +194,28 @@ var InstanceManager = class extends import_node_events.EventEmitter {
106
194
  }
107
195
  const distro = this.config.wslDistribution || "alpine-usbmuxd-build";
108
196
  try {
109
- const { stdout } = await execAsync(`wsl -d ${distro} -- ip -4 addr show eth0`);
197
+ const { stdout } = await execAsync2(`wsl -d ${distro} -- ip -4 addr show eth0`);
110
198
  const match = stdout.match(/inet\s+(\d+\.\d+\.\d+\.\d+)/);
111
199
  if (match) {
112
200
  this.wslIpAddress = match[1];
113
- logInfo(`Detected WSL IP address: ${this.wslIpAddress}`);
201
+ logInfo2(`Detected WSL IP address: ${this.wslIpAddress}`);
114
202
  return this.wslIpAddress;
115
203
  }
116
204
  } catch (error) {
117
- logWarning(`Failed to detect WSL IP via ip addr: ${error}`);
205
+ logWarning2(`Failed to detect WSL IP via ip addr: ${error}`);
118
206
  }
119
207
  try {
120
- const { stdout } = await execAsync(`wsl -d ${distro} -- hostname -I`);
208
+ const { stdout } = await execAsync2(`wsl -d ${distro} -- hostname -I`);
121
209
  const ip = stdout.trim().split(/\s+/)[0];
122
210
  if (ip && /^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
123
211
  this.wslIpAddress = ip;
124
- logInfo(`Detected WSL IP address (hostname): ${this.wslIpAddress}`);
212
+ logInfo2(`Detected WSL IP address (hostname): ${this.wslIpAddress}`);
125
213
  return this.wslIpAddress;
126
214
  }
127
215
  } catch (error) {
128
- logWarning(`Failed to detect WSL IP via hostname: ${error}`);
216
+ logWarning2(`Failed to detect WSL IP via hostname: ${error}`);
129
217
  }
130
- logWarning("Could not detect WSL IP, falling back to localhost");
218
+ logWarning2("Could not detect WSL IP, falling back to localhost");
131
219
  this.wslIpAddress = "127.0.0.1";
132
220
  return this.wslIpAddress;
133
221
  }
@@ -150,6 +238,8 @@ var InstanceManager = class extends import_node_events.EventEmitter {
150
238
  return;
151
239
  }
152
240
  this.isRunning = false;
241
+ const udids = Array.from(this.deviceMappings.keys());
242
+ await syncFromAlpine(udids, this.config);
153
243
  const detachPromises = Array.from(this.attachedDevices).map(
154
244
  (busId) => this.detachDeviceFromWsl(busId)
155
245
  );
@@ -167,28 +257,28 @@ var InstanceManager = class extends import_node_events.EventEmitter {
167
257
  */
168
258
  async findBusIdForDevice(device) {
169
259
  try {
170
- const { stdout } = await execAsync(`${USBIPD_PATH} list`);
260
+ const { stdout } = await execAsync2(`${USBIPD_PATH} list`);
171
261
  const usbipdDevices = parseUsbipdList(stdout);
172
262
  const deviceVid = device.vid.toString(16).toUpperCase().padStart(4, "0");
173
263
  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`);
264
+ logInfo2(`Looking for device with VID:PID ${deviceVid}:${devicePid}`);
265
+ logInfo2(`Found ${usbipdDevices.length} devices from usbipd list`);
176
266
  const match = usbipdDevices.find((d) => d.vid === deviceVid && d.pid === devicePid);
177
267
  if (match) {
178
- logInfo(
268
+ logInfo2(
179
269
  `Found usbipd bus ID ${match.busId} for device ${device.deviceId} (${deviceVid}:${devicePid})`
180
270
  );
181
271
  return match.busId;
182
272
  }
183
273
  for (const d of usbipdDevices) {
184
- logInfo(` usbipd device: ${d.busId} ${d.vid}:${d.pid} "${d.description}"`);
274
+ logInfo2(` usbipd device: ${d.busId} ${d.vid}:${d.pid} "${d.description}"`);
185
275
  }
186
- logWarning(
276
+ logWarning2(
187
277
  `Could not find usbipd bus ID for device ${device.deviceId} (${deviceVid}:${devicePid})`
188
278
  );
189
279
  return null;
190
280
  } catch (error) {
191
- logWarning(`Failed to run usbipd list: ${error}`);
281
+ logWarning2(`Failed to run usbipd list: ${error}`);
192
282
  return null;
193
283
  }
194
284
  }
@@ -199,15 +289,15 @@ var InstanceManager = class extends import_node_events.EventEmitter {
199
289
  const distro = this.config.wslDistribution;
200
290
  try {
201
291
  if (distro) {
202
- logInfo(`Starting WSL distribution: ${distro}...`);
203
- await execAsync(`wsl -d ${distro} -- echo "WSL started"`);
292
+ logInfo2(`Starting WSL distribution: ${distro}...`);
293
+ await execAsync2(`wsl -d ${distro} -- echo "WSL started"`);
204
294
  } else {
205
- logInfo("Starting default WSL distribution...");
206
- await execAsync(`wsl -- echo "WSL started"`);
295
+ logInfo2("Starting default WSL distribution...");
296
+ await execAsync2(`wsl -- echo "WSL started"`);
207
297
  }
208
298
  return true;
209
299
  } catch (error) {
210
- logWarning(`Failed to start WSL: ${error}`);
300
+ logWarning2(`Failed to start WSL: ${error}`);
211
301
  return false;
212
302
  }
213
303
  }
@@ -219,19 +309,24 @@ var InstanceManager = class extends import_node_events.EventEmitter {
219
309
  const distro = this.config.wslDistribution;
220
310
  try {
221
311
  await this.ensureWslRunning();
222
- logInfo(`Binding device ${busId}...`);
223
- await execAsync(`${USBIPD_PATH} bind --busid ${busId} --force`);
312
+ logInfo2(`Binding device ${busId}...`);
313
+ await execAsync2(`${USBIPD_PATH} bind --busid ${busId} --force`);
224
314
  if (distro) {
225
- logInfo(`Attaching device ${busId} to WSL distribution ${distro}...`);
226
- await execAsync(`${USBIPD_PATH} attach --wsl=${distro} --busid=${busId}`);
315
+ logInfo2(`Attaching device ${busId} to WSL distribution ${distro}...`);
316
+ await execAsync2(`${USBIPD_PATH} attach --wsl=${distro} --busid=${busId}`);
227
317
  } else {
228
- logInfo(`Attaching device ${busId} to default WSL...`);
229
- await execAsync(`${USBIPD_PATH} attach --wsl --busid=${busId}`);
318
+ logInfo2(`Attaching device ${busId} to default WSL...`);
319
+ await execAsync2(`${USBIPD_PATH} attach --wsl --busid=${busId}`);
230
320
  }
231
- logInfo(`Device ${busId} attached to WSL successfully`);
321
+ logInfo2(`Device ${busId} attached to WSL successfully`);
232
322
  return true;
233
323
  } catch (error) {
234
- logWarning(`Failed to attach device ${busId} to WSL: ${error}`);
324
+ const message = error instanceof Error ? error.message : String(error);
325
+ if (message.includes("already attached to a client")) {
326
+ logInfo2(`Device ${busId} is already attached to WSL, continuing`);
327
+ return true;
328
+ }
329
+ logWarning2(`Failed to attach device ${busId} to WSL: ${error}`);
235
330
  return false;
236
331
  }
237
332
  }
@@ -240,10 +335,15 @@ var InstanceManager = class extends import_node_events.EventEmitter {
240
335
  */
241
336
  async detachDeviceFromWsl(busId) {
242
337
  try {
243
- await execAsync(`${USBIPD_PATH} detach --busid=${busId}`);
244
- logInfo(`Device ${busId} detached from WSL`);
338
+ await execAsync2(`${USBIPD_PATH} detach --busid=${busId}`);
339
+ logInfo2(`Device ${busId} detached from WSL`);
245
340
  } catch (error) {
246
- logWarning(`Failed to detach device ${busId}: ${error}`);
341
+ const message = error instanceof Error ? error.message : String(error);
342
+ if (message.includes("no device with busid") || message.includes("There is no device")) {
343
+ logInfo2(`Device ${busId} already detached`);
344
+ } else {
345
+ logWarning2(`Failed to detach device ${busId}: ${error}`);
346
+ }
247
347
  }
248
348
  }
249
349
  /**
@@ -252,18 +352,24 @@ var InstanceManager = class extends import_node_events.EventEmitter {
252
352
  */
253
353
  async onDeviceConnected(device) {
254
354
  if (this.deviceMappings.has(device.deviceId)) {
255
- logWarning(`Device ${device.deviceId} is already connected`);
355
+ logWarning2(`Device ${device.deviceId} is already connected`);
256
356
  return;
257
357
  }
358
+ await syncToAlpine(device.deviceId, this.config);
258
359
  const busId = await this.findBusIdForDevice(device);
259
360
  if (!busId) {
260
- logWarning(`Cannot attach device ${device.deviceId} - bus ID not found`);
361
+ logWarning2(`Cannot attach device ${device.deviceId} - bus ID not found`);
261
362
  }
262
363
  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));
364
+ this.pendingAttachDevices.add(device.deviceId);
365
+ try {
366
+ const attached = await this.attachDeviceToWsl(busId);
367
+ if (attached) {
368
+ this.attachedDevices.add(busId);
369
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
370
+ }
371
+ } finally {
372
+ this.pendingAttachDevices.delete(device.deviceId);
267
373
  }
268
374
  }
269
375
  let targetInstance = this.findInstanceWithCapacity();
@@ -301,23 +407,23 @@ var InstanceManager = class extends import_node_events.EventEmitter {
301
407
  async pairDevice(udid, goIosPath = "ios") {
302
408
  const mapping = this.deviceMappings.get(udid);
303
409
  if (!mapping) {
304
- logWarning(`Cannot pair device ${udid} - not found in mappings`);
410
+ logWarning2(`Cannot pair device ${udid} - not found in mappings`);
305
411
  return false;
306
412
  }
307
413
  try {
308
414
  const socketAddress = `${mapping.host}:${mapping.port}`;
309
- logInfo(`Pairing device ${udid} via ${socketAddress}...`);
310
- const { stderr } = await execAsync(`"${goIosPath}" pair --udid=${udid}`, {
415
+ logInfo2(`Pairing device ${udid} via ${socketAddress}...`);
416
+ const { stderr } = await execAsync2(`"${goIosPath}" pair --udid=${udid}`, {
311
417
  env: { ...process.env, USBMUXD_SOCKET_ADDRESS: socketAddress }
312
418
  });
313
419
  if (stderr?.includes("error")) {
314
- logWarning(`Pairing warning for ${udid}: ${stderr}`);
420
+ logWarning2(`Pairing warning for ${udid}: ${stderr}`);
315
421
  }
316
- logInfo(`Device ${udid} paired successfully`);
422
+ logInfo2(`Device ${udid} paired successfully`);
317
423
  this.emit("device-paired", { udid, mapping });
318
424
  return true;
319
425
  } catch (error) {
320
- logWarning(`Failed to pair device ${udid}: ${error}`);
426
+ logWarning2(`Failed to pair device ${udid}: ${error}`);
321
427
  return false;
322
428
  }
323
429
  }
@@ -326,9 +432,12 @@ var InstanceManager = class extends import_node_events.EventEmitter {
326
432
  * Detaches from WSL, removes device from instance, and stops instance if empty
327
433
  */
328
434
  async onDeviceDisconnected(device) {
435
+ if (this.pendingAttachDevices.has(device.deviceId)) {
436
+ return;
437
+ }
329
438
  const mapping = this.deviceMappings.get(device.deviceId);
330
439
  if (!mapping) {
331
- logWarning(`Device ${device.deviceId} was not tracked`);
440
+ logWarning2(`Device ${device.deviceId} was not tracked`);
332
441
  return;
333
442
  }
334
443
  if (mapping.busId) {
@@ -337,7 +446,7 @@ var InstanceManager = class extends import_node_events.EventEmitter {
337
446
  }
338
447
  const instance = this.instances.get(mapping.instanceId);
339
448
  if (!instance) {
340
- logWarning(`Instance ${mapping.instanceId} not found`);
449
+ logWarning2(`Instance ${mapping.instanceId} not found`);
341
450
  this.deviceMappings.delete(device.deviceId);
342
451
  return;
343
452
  }
@@ -392,7 +501,7 @@ var InstanceManager = class extends import_node_events.EventEmitter {
392
501
  this.config.usbmuxdPath,
393
502
  ...usbmuxdArgs
394
503
  ];
395
- const process2 = (0, import_node_child_process.spawn)("wsl", wslArgs, {
504
+ const process2 = (0, import_node_child_process2.spawn)("wsl", wslArgs, {
396
505
  stdio: ["ignore", "pipe", "pipe"],
397
506
  windowsHide: false
398
507
  // Show console for debugging
@@ -514,7 +623,7 @@ var InstanceManager = class extends import_node_events.EventEmitter {
514
623
  };
515
624
 
516
625
  // src/UsbmuxdService.ts
517
- var { logInfo: logInfo2, logError } = (0, import_tool_debug_g42.createLoggers)("usbmuxd-instance-manager");
626
+ var { logInfo: logInfo3, logError } = (0, import_tool_debug_g43.createLoggers)("usbmuxd-instance-manager");
518
627
  var UsbmuxdService = class extends import_node_events2.EventEmitter {
519
628
  manager;
520
629
  usbListener;
@@ -573,17 +682,17 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
573
682
  }
574
683
  const config = this.manager.getConfig();
575
684
  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})`);
685
+ logInfo3("Starting service...");
686
+ logInfo3(`Batch size: ${config.batchSize} devices per instance`);
687
+ logInfo3(`Base port: ${config.basePort}`);
688
+ logInfo3(`Max instances: ${config.maxInstances}`);
689
+ logInfo3(`usbmuxd path: ${config.usbmuxdPath}`);
690
+ logInfo3(`Monitoring Apple devices (VID: ${config.appleVendorId})`);
582
691
  this.usbListener.onDeviceAdd(async (device) => {
583
692
  if (device.vid !== appleVid) {
584
693
  return;
585
694
  }
586
- logInfo2(`Apple device connected: ${device.deviceId} (${device.deviceName || "Unknown"})`);
695
+ logInfo3(`Apple device connected: ${device.deviceId} (${device.deviceName || "Unknown"})`);
587
696
  try {
588
697
  await this.manager.onDeviceConnected(device);
589
698
  } catch (error) {
@@ -594,7 +703,7 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
594
703
  if (device.vid !== appleVid) {
595
704
  return;
596
705
  }
597
- logInfo2(`Apple device disconnected: ${device.deviceId}`);
706
+ logInfo3(`Apple device disconnected: ${device.deviceId}`);
598
707
  try {
599
708
  await this.manager.onDeviceDisconnected(device);
600
709
  } catch (error) {
@@ -608,8 +717,8 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
608
717
  });
609
718
  this.isListening = true;
610
719
  this.manager.start();
611
- logInfo2("Service started successfully");
612
- logInfo2("Waiting for iOS devices...");
720
+ logInfo3("Service started successfully");
721
+ logInfo3("Waiting for iOS devices...");
613
722
  } catch (error) {
614
723
  logError("Failed to start USB listener:", error);
615
724
  throw error;
@@ -623,11 +732,11 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
623
732
  if (!this.isListening) {
624
733
  return;
625
734
  }
626
- logInfo2("Stopping service...");
735
+ logInfo3("Stopping service...");
627
736
  this.usbListener.stopListening();
628
737
  this.isListening = false;
629
738
  await this.manager.stop();
630
- logInfo2("Service stopped");
739
+ logInfo3("Service stopped");
631
740
  }
632
741
  /**
633
742
  * Get device port mapping