@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/README.md +74 -1
- package/dist/cli.js +194 -85
- package/dist/cli.js.map +4 -4
- package/dist/cli.mjs +193 -84
- package/dist/cli.mjs.map +4 -4
- package/dist/index.js +170 -61
- package/dist/index.js.map +4 -4
- package/dist/index.mjs +169 -60
- package/dist/index.mjs.map +4 -4
- package/dist/types/InstanceManager.d.ts +2 -0
- package/dist/types/InstanceManager.d.ts.map +1 -1
- package/dist/types/LockdownSync.d.ts +16 -0
- package/dist/types/LockdownSync.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +12 -0
- package/dist/types/types/index.d.ts.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -108,6 +108,19 @@ await service.stop();
|
|
|
108
108
|
| `wslDistribution` | string | `usbmuxd-alpine` | Alpine WSL2 distribution name |
|
|
109
109
|
| `verboseLogging` | boolean | true | Enable verbose output |
|
|
110
110
|
| `appleVendorId` | string | '05AC' | Apple vendor ID (hex) |
|
|
111
|
+
| `lockdownWindowsPath` | string | `C:\ProgramData\mce\lockdown` | Windows folder for lockdown (pairing) files; synced to/from Alpine |
|
|
112
|
+
| `lockdownSyncEnabled` | boolean | true | Sync lockdown files so pairing can be skipped |
|
|
113
|
+
|
|
114
|
+
### Lockdown sync
|
|
115
|
+
|
|
116
|
+
When enabled, the manager syncs lockdown (pairing) files between Windows and Alpine so devices that were already paired on Windows can be used without pairing again in WSL:
|
|
117
|
+
|
|
118
|
+
- **To Alpine**: When a device is assigned, that device’s plist (`{UDID}.plist`) is copied from `lockdownWindowsPath` to Alpine `/var/lib/lockdown`. Only the file for that device is copied.
|
|
119
|
+
- **From Alpine**: When the service stops, plists for all devices that were assigned this session (plus `SystemConfiguration.plist` if present) are copied from Alpine back to `lockdownWindowsPath` so they are available for the next run.
|
|
120
|
+
|
|
121
|
+
**Alpine permissions**: `/var/lib/lockdown` is often root-owned. The sync tries a normal copy first; if that fails (e.g. permission denied), it retries with `sudo cp`. Ensure the WSL user has passwordless sudo for these commands, or that the Alpine image has `/var/lib/lockdown` writable by the default user. If both fail, a warning is logged and device assignment continues (pairing can still be done manually).
|
|
122
|
+
|
|
123
|
+
Set `lockdownSyncEnabled: false` or leave `lockdownWindowsPath` empty to disable lockdown sync.
|
|
111
124
|
|
|
112
125
|
## CLI Options
|
|
113
126
|
|
|
@@ -118,7 +131,8 @@ Options:
|
|
|
118
131
|
--batchSize <n> Number of devices per instance (default: 4)
|
|
119
132
|
--basePort <port> Base TCP port for first instance (default: 27015)
|
|
120
133
|
--maxInstances <n> Maximum number of instances (default: 20)
|
|
121
|
-
--usbmuxdPath <path> Path to usbmuxd executable
|
|
134
|
+
--usbmuxdPath <path> Path to usbmuxd executable (WSL: default usbmuxd)
|
|
135
|
+
--wslDistribution <n> WSL distribution name (default: alpine-usbmuxd-build)
|
|
122
136
|
--verbose <true|false> Enable verbose logging (default: true)
|
|
123
137
|
--appleVid <vid> Apple Vendor ID in hex (default: 05AC)
|
|
124
138
|
-h, --help Show this help message
|
|
@@ -162,6 +176,65 @@ When all devices disconnect from Instance 2:
|
|
|
162
176
|
- Instance 2 automatically terminates
|
|
163
177
|
- Frees resources
|
|
164
178
|
|
|
179
|
+
## Flow: Working with usbipd and WSL
|
|
180
|
+
|
|
181
|
+
On Windows, the same physical USB device can be used by **either** Windows (e.g. iTunes, Apple Mobile Device) **or** WSL (usbmuxd), not both. usbipd-win is what moves the device between the two.
|
|
182
|
+
|
|
183
|
+
### Device ownership
|
|
184
|
+
|
|
185
|
+
| State | Who has the device | iTunes / Windows | usbmuxd (WSL) |
|
|
186
|
+
|-------|--------------------|------------------|---------------|
|
|
187
|
+
| Not shared | Windows | ✓ | ✗ |
|
|
188
|
+
| Bound + attached to WSL | WSL | ✗ | ✓ |
|
|
189
|
+
| Bound but detached | Neither (held by usbipd) | ✗ | ✗ |
|
|
190
|
+
|
|
191
|
+
### Giving the device to usbmuxd (instance manager)
|
|
192
|
+
|
|
193
|
+
1. **WSL** must be running (e.g. the manager starts it, or run `wsl -d alpine-usbmuxd-build -- echo ok`).
|
|
194
|
+
2. **Bind** the device (take it from Windows):
|
|
195
|
+
`usbipd bind --busid <BUSID> --force`
|
|
196
|
+
3. **Attach** to WSL:
|
|
197
|
+
`usbipd attach --wsl=<distro> --busid=<BUSID>`
|
|
198
|
+
|
|
199
|
+
The instance manager does steps 2–3 automatically when it sees an iOS device. You can also use the script:
|
|
200
|
+
|
|
201
|
+
```powershell
|
|
202
|
+
# From packages/usbmuxd-instance-manager
|
|
203
|
+
.\scripts\attach-device.ps1
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Giving the device back to Windows (e.g. for iTunes)
|
|
207
|
+
|
|
208
|
+
1. **Detach** from WSL (stops forwarding to Linux):
|
|
209
|
+
`usbipd detach --busid <BUSID>`
|
|
210
|
+
Or detach all:
|
|
211
|
+
`usbipd detach -a`
|
|
212
|
+
2. **Unbind** so Windows gets the device back:
|
|
213
|
+
`usbipd unbind --busid <BUSID>`
|
|
214
|
+
|
|
215
|
+
Until you unbind, the device stays “Shared (forced)” and Windows/iTunes will not see it.
|
|
216
|
+
|
|
217
|
+
### Useful usbipd commands
|
|
218
|
+
|
|
219
|
+
```powershell
|
|
220
|
+
$usbipd = "C:\Program Files\usbipd-win\usbipd.exe"
|
|
221
|
+
|
|
222
|
+
# List devices and their state (Shared / Not shared, Attached / Not attached)
|
|
223
|
+
& $usbipd list
|
|
224
|
+
|
|
225
|
+
# Detach from WSL (device still bound to usbipd)
|
|
226
|
+
& $usbipd detach --busid 2-5
|
|
227
|
+
& $usbipd detach -a
|
|
228
|
+
|
|
229
|
+
# Unbind so Windows can use the device again (e.g. iTunes)
|
|
230
|
+
& $usbipd unbind --busid 2-5
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Quick reference
|
|
234
|
+
|
|
235
|
+
- **Use device with usbmuxd-instance-manager** → bind + attach (manager or script does this when you connect a device).
|
|
236
|
+
- **Use device with iTunes on Windows** → detach then unbind for that bus ID; then iTunes will see it.
|
|
237
|
+
|
|
165
238
|
## Requirements
|
|
166
239
|
|
|
167
240
|
### Runtime
|
package/dist/cli.js
CHANGED
|
@@ -24,20 +24,104 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
|
-
var
|
|
27
|
+
var import_tool_debug_g44 = require("@mcesystems/tool-debug-g4");
|
|
28
28
|
|
|
29
29
|
// src/UsbmuxdService.ts
|
|
30
30
|
var import_node_events2 = require("node:events");
|
|
31
|
-
var
|
|
31
|
+
var import_tool_debug_g43 = require("@mcesystems/tool-debug-g4");
|
|
32
32
|
var import_usb_device_listener = __toESM(require("@mcesystems/usb-device-listener"));
|
|
33
33
|
|
|
34
34
|
// src/InstanceManager.ts
|
|
35
|
-
var
|
|
35
|
+
var import_node_child_process2 = require("node:child_process");
|
|
36
36
|
var import_node_events = require("node:events");
|
|
37
|
+
var import_node_util2 = require("node:util");
|
|
38
|
+
var import_tool_debug_g42 = require("@mcesystems/tool-debug-g4");
|
|
39
|
+
|
|
40
|
+
// src/LockdownSync.ts
|
|
41
|
+
var import_node_child_process = require("node:child_process");
|
|
42
|
+
var import_node_fs = require("node:fs");
|
|
43
|
+
var import_node_path = require("node:path");
|
|
37
44
|
var import_node_util = require("node:util");
|
|
38
45
|
var import_tool_debug_g4 = require("@mcesystems/tool-debug-g4");
|
|
39
46
|
var { logInfo, logWarning } = (0, import_tool_debug_g4.createLoggers)("usbmuxd-instance-manager");
|
|
40
47
|
var execAsync = (0, import_node_util.promisify)(import_node_child_process.exec);
|
|
48
|
+
var ALPINE_LOCKDOWN_DIR = "/var/lib/lockdown";
|
|
49
|
+
var SYSTEM_CONFIG_PLIST = "SystemConfiguration.plist";
|
|
50
|
+
function windowsPathToWsl(windowsPath) {
|
|
51
|
+
const normalized = windowsPath.replace(/\\/g, "/").trim();
|
|
52
|
+
const driveMatch = normalized.match(/^([a-zA-Z]):\/?(.*)$/);
|
|
53
|
+
if (driveMatch) {
|
|
54
|
+
const drive = driveMatch[1].toLowerCase();
|
|
55
|
+
const rest = driveMatch[2] || "";
|
|
56
|
+
return `/mnt/${drive}${rest ? `/${rest}` : ""}`;
|
|
57
|
+
}
|
|
58
|
+
return normalized;
|
|
59
|
+
}
|
|
60
|
+
async function syncToAlpine(udid, config2) {
|
|
61
|
+
if (!config2.lockdownSyncEnabled || !config2.lockdownWindowsPath?.trim()) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const windowsDir = config2.lockdownWindowsPath.trim();
|
|
65
|
+
const windowsFile = (0, import_node_path.join)(windowsDir, `${udid}.plist`);
|
|
66
|
+
if (!(0, import_node_fs.existsSync)(windowsFile)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const distro = config2.wslDistribution || "alpine-usbmuxd-build";
|
|
70
|
+
const wslSource = windowsPathToWsl(windowsFile);
|
|
71
|
+
const wslDestDir = ALPINE_LOCKDOWN_DIR;
|
|
72
|
+
try {
|
|
73
|
+
await execAsync(
|
|
74
|
+
`wsl -d ${distro} -- sh -c "mkdir -p ${wslDestDir} && cp '${wslSource}' '${wslDestDir}/'"`
|
|
75
|
+
);
|
|
76
|
+
logInfo(`Lockdown sync: copied ${udid}.plist to Alpine`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
try {
|
|
79
|
+
await execAsync(
|
|
80
|
+
`wsl -d ${distro} -- sh -c "mkdir -p ${wslDestDir} && sudo cp '${wslSource}' '${wslDestDir}/'"`
|
|
81
|
+
);
|
|
82
|
+
logInfo(`Lockdown sync: copied ${udid}.plist to Alpine (via sudo)`);
|
|
83
|
+
} catch (sudoError) {
|
|
84
|
+
logWarning(
|
|
85
|
+
`Lockdown sync to Alpine failed for ${udid}: ${error}. Sudo fallback failed: ${sudoError}. Ensure /var/lib/lockdown is writable or use passwordless sudo.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function syncFromAlpine(udids, config2) {
|
|
91
|
+
if (!config2.lockdownSyncEnabled || !config2.lockdownWindowsPath?.trim()) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const windowsDir = config2.lockdownWindowsPath.trim();
|
|
95
|
+
try {
|
|
96
|
+
(0, import_node_fs.mkdirSync)(windowsDir, { recursive: true });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
logWarning(`Lockdown sync: could not create Windows dir ${windowsDir}: ${error}`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const distro = config2.wslDistribution || "alpine-usbmuxd-build";
|
|
102
|
+
const wslDestDir = windowsPathToWsl(windowsDir);
|
|
103
|
+
for (const udid of udids) {
|
|
104
|
+
const plist = `${udid}.plist`;
|
|
105
|
+
try {
|
|
106
|
+
await execAsync(
|
|
107
|
+
`wsl -d ${distro} -- sh -c "test -f '${ALPINE_LOCKDOWN_DIR}/${plist}' && cp '${ALPINE_LOCKDOWN_DIR}/${plist}' '${wslDestDir}/'"`
|
|
108
|
+
);
|
|
109
|
+
logInfo(`Lockdown sync: copied ${plist} from Alpine to Windows`);
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await execAsync(
|
|
115
|
+
`wsl -d ${distro} -- sh -c "test -f '${ALPINE_LOCKDOWN_DIR}/${SYSTEM_CONFIG_PLIST}' && cp '${ALPINE_LOCKDOWN_DIR}/${SYSTEM_CONFIG_PLIST}' '${wslDestDir}/'"`
|
|
116
|
+
);
|
|
117
|
+
logInfo(`Lockdown sync: copied ${SYSTEM_CONFIG_PLIST} from Alpine to Windows`);
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/InstanceManager.ts
|
|
123
|
+
var { logInfo: logInfo2, logWarning: logWarning2 } = (0, import_tool_debug_g42.createLoggers)("usbmuxd-instance-manager");
|
|
124
|
+
var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.exec);
|
|
41
125
|
var USBIPD_PATH = '"C:\\Program Files\\usbipd-win\\usbipd.exe"';
|
|
42
126
|
function parseUsbipdList(output) {
|
|
43
127
|
const devices = [];
|
|
@@ -69,7 +153,9 @@ var DEFAULT_CONFIG = {
|
|
|
69
153
|
wslDistribution: "alpine-usbmuxd-build",
|
|
70
154
|
// Alpine WSL2 distribution name
|
|
71
155
|
verboseLogging: true,
|
|
72
|
-
appleVendorId: "05AC"
|
|
156
|
+
appleVendorId: "05AC",
|
|
157
|
+
lockdownWindowsPath: "C:\\ProgramData\\mce\\lockdown",
|
|
158
|
+
lockdownSyncEnabled: true
|
|
73
159
|
};
|
|
74
160
|
var InstanceManager = class extends import_node_events.EventEmitter {
|
|
75
161
|
config;
|
|
@@ -81,6 +167,8 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
81
167
|
isRunning = false;
|
|
82
168
|
/** Tracks which devices have been attached to WSL */
|
|
83
169
|
attachedDevices = /* @__PURE__ */ new Set();
|
|
170
|
+
/** Device IDs currently in the attach flow (ignore disconnect until attach completes) */
|
|
171
|
+
pendingAttachDevices = /* @__PURE__ */ new Set();
|
|
84
172
|
/** Cached WSL IP address for connecting from Windows */
|
|
85
173
|
wslIpAddress = null;
|
|
86
174
|
constructor(config2 = {}) {
|
|
@@ -97,28 +185,28 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
97
185
|
}
|
|
98
186
|
const distro = this.config.wslDistribution || "alpine-usbmuxd-build";
|
|
99
187
|
try {
|
|
100
|
-
const { stdout } = await
|
|
188
|
+
const { stdout } = await execAsync2(`wsl -d ${distro} -- ip -4 addr show eth0`);
|
|
101
189
|
const match = stdout.match(/inet\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
102
190
|
if (match) {
|
|
103
191
|
this.wslIpAddress = match[1];
|
|
104
|
-
|
|
192
|
+
logInfo2(`Detected WSL IP address: ${this.wslIpAddress}`);
|
|
105
193
|
return this.wslIpAddress;
|
|
106
194
|
}
|
|
107
195
|
} catch (error) {
|
|
108
|
-
|
|
196
|
+
logWarning2(`Failed to detect WSL IP via ip addr: ${error}`);
|
|
109
197
|
}
|
|
110
198
|
try {
|
|
111
|
-
const { stdout } = await
|
|
199
|
+
const { stdout } = await execAsync2(`wsl -d ${distro} -- hostname -I`);
|
|
112
200
|
const ip = stdout.trim().split(/\s+/)[0];
|
|
113
201
|
if (ip && /^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
|
|
114
202
|
this.wslIpAddress = ip;
|
|
115
|
-
|
|
203
|
+
logInfo2(`Detected WSL IP address (hostname): ${this.wslIpAddress}`);
|
|
116
204
|
return this.wslIpAddress;
|
|
117
205
|
}
|
|
118
206
|
} catch (error) {
|
|
119
|
-
|
|
207
|
+
logWarning2(`Failed to detect WSL IP via hostname: ${error}`);
|
|
120
208
|
}
|
|
121
|
-
|
|
209
|
+
logWarning2("Could not detect WSL IP, falling back to localhost");
|
|
122
210
|
this.wslIpAddress = "127.0.0.1";
|
|
123
211
|
return this.wslIpAddress;
|
|
124
212
|
}
|
|
@@ -141,6 +229,8 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
141
229
|
return;
|
|
142
230
|
}
|
|
143
231
|
this.isRunning = false;
|
|
232
|
+
const udids = Array.from(this.deviceMappings.keys());
|
|
233
|
+
await syncFromAlpine(udids, this.config);
|
|
144
234
|
const detachPromises = Array.from(this.attachedDevices).map(
|
|
145
235
|
(busId) => this.detachDeviceFromWsl(busId)
|
|
146
236
|
);
|
|
@@ -158,28 +248,28 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
158
248
|
*/
|
|
159
249
|
async findBusIdForDevice(device) {
|
|
160
250
|
try {
|
|
161
|
-
const { stdout } = await
|
|
251
|
+
const { stdout } = await execAsync2(`${USBIPD_PATH} list`);
|
|
162
252
|
const usbipdDevices = parseUsbipdList(stdout);
|
|
163
253
|
const deviceVid = device.vid.toString(16).toUpperCase().padStart(4, "0");
|
|
164
254
|
const devicePid = device.pid.toString(16).toUpperCase().padStart(4, "0");
|
|
165
|
-
|
|
166
|
-
|
|
255
|
+
logInfo2(`Looking for device with VID:PID ${deviceVid}:${devicePid}`);
|
|
256
|
+
logInfo2(`Found ${usbipdDevices.length} devices from usbipd list`);
|
|
167
257
|
const match = usbipdDevices.find((d) => d.vid === deviceVid && d.pid === devicePid);
|
|
168
258
|
if (match) {
|
|
169
|
-
|
|
259
|
+
logInfo2(
|
|
170
260
|
`Found usbipd bus ID ${match.busId} for device ${device.deviceId} (${deviceVid}:${devicePid})`
|
|
171
261
|
);
|
|
172
262
|
return match.busId;
|
|
173
263
|
}
|
|
174
264
|
for (const d of usbipdDevices) {
|
|
175
|
-
|
|
265
|
+
logInfo2(` usbipd device: ${d.busId} ${d.vid}:${d.pid} "${d.description}"`);
|
|
176
266
|
}
|
|
177
|
-
|
|
267
|
+
logWarning2(
|
|
178
268
|
`Could not find usbipd bus ID for device ${device.deviceId} (${deviceVid}:${devicePid})`
|
|
179
269
|
);
|
|
180
270
|
return null;
|
|
181
271
|
} catch (error) {
|
|
182
|
-
|
|
272
|
+
logWarning2(`Failed to run usbipd list: ${error}`);
|
|
183
273
|
return null;
|
|
184
274
|
}
|
|
185
275
|
}
|
|
@@ -190,15 +280,15 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
190
280
|
const distro = this.config.wslDistribution;
|
|
191
281
|
try {
|
|
192
282
|
if (distro) {
|
|
193
|
-
|
|
194
|
-
await
|
|
283
|
+
logInfo2(`Starting WSL distribution: ${distro}...`);
|
|
284
|
+
await execAsync2(`wsl -d ${distro} -- echo "WSL started"`);
|
|
195
285
|
} else {
|
|
196
|
-
|
|
197
|
-
await
|
|
286
|
+
logInfo2("Starting default WSL distribution...");
|
|
287
|
+
await execAsync2(`wsl -- echo "WSL started"`);
|
|
198
288
|
}
|
|
199
289
|
return true;
|
|
200
290
|
} catch (error) {
|
|
201
|
-
|
|
291
|
+
logWarning2(`Failed to start WSL: ${error}`);
|
|
202
292
|
return false;
|
|
203
293
|
}
|
|
204
294
|
}
|
|
@@ -210,19 +300,24 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
210
300
|
const distro = this.config.wslDistribution;
|
|
211
301
|
try {
|
|
212
302
|
await this.ensureWslRunning();
|
|
213
|
-
|
|
214
|
-
await
|
|
303
|
+
logInfo2(`Binding device ${busId}...`);
|
|
304
|
+
await execAsync2(`${USBIPD_PATH} bind --busid ${busId} --force`);
|
|
215
305
|
if (distro) {
|
|
216
|
-
|
|
217
|
-
await
|
|
306
|
+
logInfo2(`Attaching device ${busId} to WSL distribution ${distro}...`);
|
|
307
|
+
await execAsync2(`${USBIPD_PATH} attach --wsl=${distro} --busid=${busId}`);
|
|
218
308
|
} else {
|
|
219
|
-
|
|
220
|
-
await
|
|
309
|
+
logInfo2(`Attaching device ${busId} to default WSL...`);
|
|
310
|
+
await execAsync2(`${USBIPD_PATH} attach --wsl --busid=${busId}`);
|
|
221
311
|
}
|
|
222
|
-
|
|
312
|
+
logInfo2(`Device ${busId} attached to WSL successfully`);
|
|
223
313
|
return true;
|
|
224
314
|
} catch (error) {
|
|
225
|
-
|
|
315
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
316
|
+
if (message.includes("already attached to a client")) {
|
|
317
|
+
logInfo2(`Device ${busId} is already attached to WSL, continuing`);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
logWarning2(`Failed to attach device ${busId} to WSL: ${error}`);
|
|
226
321
|
return false;
|
|
227
322
|
}
|
|
228
323
|
}
|
|
@@ -231,10 +326,15 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
231
326
|
*/
|
|
232
327
|
async detachDeviceFromWsl(busId) {
|
|
233
328
|
try {
|
|
234
|
-
await
|
|
235
|
-
|
|
329
|
+
await execAsync2(`${USBIPD_PATH} detach --busid=${busId}`);
|
|
330
|
+
logInfo2(`Device ${busId} detached from WSL`);
|
|
236
331
|
} catch (error) {
|
|
237
|
-
|
|
332
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
333
|
+
if (message.includes("no device with busid") || message.includes("There is no device")) {
|
|
334
|
+
logInfo2(`Device ${busId} already detached`);
|
|
335
|
+
} else {
|
|
336
|
+
logWarning2(`Failed to detach device ${busId}: ${error}`);
|
|
337
|
+
}
|
|
238
338
|
}
|
|
239
339
|
}
|
|
240
340
|
/**
|
|
@@ -243,18 +343,24 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
243
343
|
*/
|
|
244
344
|
async onDeviceConnected(device) {
|
|
245
345
|
if (this.deviceMappings.has(device.deviceId)) {
|
|
246
|
-
|
|
346
|
+
logWarning2(`Device ${device.deviceId} is already connected`);
|
|
247
347
|
return;
|
|
248
348
|
}
|
|
349
|
+
await syncToAlpine(device.deviceId, this.config);
|
|
249
350
|
const busId = await this.findBusIdForDevice(device);
|
|
250
351
|
if (!busId) {
|
|
251
|
-
|
|
352
|
+
logWarning2(`Cannot attach device ${device.deviceId} - bus ID not found`);
|
|
252
353
|
}
|
|
253
354
|
if (busId && !this.attachedDevices.has(busId)) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
this.
|
|
257
|
-
|
|
355
|
+
this.pendingAttachDevices.add(device.deviceId);
|
|
356
|
+
try {
|
|
357
|
+
const attached = await this.attachDeviceToWsl(busId);
|
|
358
|
+
if (attached) {
|
|
359
|
+
this.attachedDevices.add(busId);
|
|
360
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
361
|
+
}
|
|
362
|
+
} finally {
|
|
363
|
+
this.pendingAttachDevices.delete(device.deviceId);
|
|
258
364
|
}
|
|
259
365
|
}
|
|
260
366
|
let targetInstance = this.findInstanceWithCapacity();
|
|
@@ -292,23 +398,23 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
292
398
|
async pairDevice(udid, goIosPath = "ios") {
|
|
293
399
|
const mapping = this.deviceMappings.get(udid);
|
|
294
400
|
if (!mapping) {
|
|
295
|
-
|
|
401
|
+
logWarning2(`Cannot pair device ${udid} - not found in mappings`);
|
|
296
402
|
return false;
|
|
297
403
|
}
|
|
298
404
|
try {
|
|
299
405
|
const socketAddress = `${mapping.host}:${mapping.port}`;
|
|
300
|
-
|
|
301
|
-
const { stderr } = await
|
|
406
|
+
logInfo2(`Pairing device ${udid} via ${socketAddress}...`);
|
|
407
|
+
const { stderr } = await execAsync2(`"${goIosPath}" pair --udid=${udid}`, {
|
|
302
408
|
env: { ...process.env, USBMUXD_SOCKET_ADDRESS: socketAddress }
|
|
303
409
|
});
|
|
304
410
|
if (stderr?.includes("error")) {
|
|
305
|
-
|
|
411
|
+
logWarning2(`Pairing warning for ${udid}: ${stderr}`);
|
|
306
412
|
}
|
|
307
|
-
|
|
413
|
+
logInfo2(`Device ${udid} paired successfully`);
|
|
308
414
|
this.emit("device-paired", { udid, mapping });
|
|
309
415
|
return true;
|
|
310
416
|
} catch (error) {
|
|
311
|
-
|
|
417
|
+
logWarning2(`Failed to pair device ${udid}: ${error}`);
|
|
312
418
|
return false;
|
|
313
419
|
}
|
|
314
420
|
}
|
|
@@ -317,9 +423,12 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
317
423
|
* Detaches from WSL, removes device from instance, and stops instance if empty
|
|
318
424
|
*/
|
|
319
425
|
async onDeviceDisconnected(device) {
|
|
426
|
+
if (this.pendingAttachDevices.has(device.deviceId)) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
320
429
|
const mapping = this.deviceMappings.get(device.deviceId);
|
|
321
430
|
if (!mapping) {
|
|
322
|
-
|
|
431
|
+
logWarning2(`Device ${device.deviceId} was not tracked`);
|
|
323
432
|
return;
|
|
324
433
|
}
|
|
325
434
|
if (mapping.busId) {
|
|
@@ -328,7 +437,7 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
328
437
|
}
|
|
329
438
|
const instance = this.instances.get(mapping.instanceId);
|
|
330
439
|
if (!instance) {
|
|
331
|
-
|
|
440
|
+
logWarning2(`Instance ${mapping.instanceId} not found`);
|
|
332
441
|
this.deviceMappings.delete(device.deviceId);
|
|
333
442
|
return;
|
|
334
443
|
}
|
|
@@ -383,7 +492,7 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
383
492
|
this.config.usbmuxdPath,
|
|
384
493
|
...usbmuxdArgs
|
|
385
494
|
];
|
|
386
|
-
const process2 = (0,
|
|
495
|
+
const process2 = (0, import_node_child_process2.spawn)("wsl", wslArgs, {
|
|
387
496
|
stdio: ["ignore", "pipe", "pipe"],
|
|
388
497
|
windowsHide: false
|
|
389
498
|
// Show console for debugging
|
|
@@ -505,7 +614,7 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
505
614
|
};
|
|
506
615
|
|
|
507
616
|
// src/UsbmuxdService.ts
|
|
508
|
-
var { logInfo:
|
|
617
|
+
var { logInfo: logInfo3, logError } = (0, import_tool_debug_g43.createLoggers)("usbmuxd-instance-manager");
|
|
509
618
|
var UsbmuxdService = class extends import_node_events2.EventEmitter {
|
|
510
619
|
manager;
|
|
511
620
|
usbListener;
|
|
@@ -564,17 +673,17 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
|
|
|
564
673
|
}
|
|
565
674
|
const config2 = this.manager.getConfig();
|
|
566
675
|
const appleVid = Number.parseInt(config2.appleVendorId, 16);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
676
|
+
logInfo3("Starting service...");
|
|
677
|
+
logInfo3(`Batch size: ${config2.batchSize} devices per instance`);
|
|
678
|
+
logInfo3(`Base port: ${config2.basePort}`);
|
|
679
|
+
logInfo3(`Max instances: ${config2.maxInstances}`);
|
|
680
|
+
logInfo3(`usbmuxd path: ${config2.usbmuxdPath}`);
|
|
681
|
+
logInfo3(`Monitoring Apple devices (VID: ${config2.appleVendorId})`);
|
|
573
682
|
this.usbListener.onDeviceAdd(async (device) => {
|
|
574
683
|
if (device.vid !== appleVid) {
|
|
575
684
|
return;
|
|
576
685
|
}
|
|
577
|
-
|
|
686
|
+
logInfo3(`Apple device connected: ${device.deviceId} (${device.deviceName || "Unknown"})`);
|
|
578
687
|
try {
|
|
579
688
|
await this.manager.onDeviceConnected(device);
|
|
580
689
|
} catch (error) {
|
|
@@ -585,7 +694,7 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
|
|
|
585
694
|
if (device.vid !== appleVid) {
|
|
586
695
|
return;
|
|
587
696
|
}
|
|
588
|
-
|
|
697
|
+
logInfo3(`Apple device disconnected: ${device.deviceId}`);
|
|
589
698
|
try {
|
|
590
699
|
await this.manager.onDeviceDisconnected(device);
|
|
591
700
|
} catch (error) {
|
|
@@ -599,8 +708,8 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
|
|
|
599
708
|
});
|
|
600
709
|
this.isListening = true;
|
|
601
710
|
this.manager.start();
|
|
602
|
-
|
|
603
|
-
|
|
711
|
+
logInfo3("Service started successfully");
|
|
712
|
+
logInfo3("Waiting for iOS devices...");
|
|
604
713
|
} catch (error) {
|
|
605
714
|
logError("Failed to start USB listener:", error);
|
|
606
715
|
throw error;
|
|
@@ -614,11 +723,11 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
|
|
|
614
723
|
if (!this.isListening) {
|
|
615
724
|
return;
|
|
616
725
|
}
|
|
617
|
-
|
|
726
|
+
logInfo3("Stopping service...");
|
|
618
727
|
this.usbListener.stopListening();
|
|
619
728
|
this.isListening = false;
|
|
620
729
|
await this.manager.stop();
|
|
621
|
-
|
|
730
|
+
logInfo3("Service stopped");
|
|
622
731
|
}
|
|
623
732
|
/**
|
|
624
733
|
* Get device port mapping
|
|
@@ -665,7 +774,7 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
|
|
|
665
774
|
};
|
|
666
775
|
|
|
667
776
|
// src/cli.ts
|
|
668
|
-
var { logInfo:
|
|
777
|
+
var { logInfo: logInfo4, logError: logError2, logHeader, logDataObject } = (0, import_tool_debug_g44.createLoggers)("usbmuxd-cli");
|
|
669
778
|
var args = process.argv.slice(2);
|
|
670
779
|
var options = {};
|
|
671
780
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -682,7 +791,7 @@ var config = {
|
|
|
682
791
|
batchSize: options.batchSize ? Number.parseInt(options.batchSize, 10) : 4,
|
|
683
792
|
basePort: options.basePort ? Number.parseInt(options.basePort, 10) : 27015,
|
|
684
793
|
maxInstances: options.maxInstances ? Number.parseInt(options.maxInstances, 10) : 20,
|
|
685
|
-
usbmuxdPath: options.usbmuxdPath || "
|
|
794
|
+
usbmuxdPath: options.usbmuxdPath || "usbmuxd",
|
|
686
795
|
verboseLogging: options.verbose !== "false",
|
|
687
796
|
appleVendorId: options.appleVid || "05AC"
|
|
688
797
|
};
|
|
@@ -690,11 +799,11 @@ logHeader("usbmuxd Instance Manager");
|
|
|
690
799
|
logDataObject("Multi-instance manager for iOS device connections", { config });
|
|
691
800
|
var service = new UsbmuxdService(config);
|
|
692
801
|
process.on("SIGINT", async () => {
|
|
693
|
-
|
|
694
|
-
|
|
802
|
+
logInfo4("");
|
|
803
|
+
logInfo4("Received SIGINT, shutting down gracefully...");
|
|
695
804
|
try {
|
|
696
805
|
await service.stop();
|
|
697
|
-
|
|
806
|
+
logInfo4("Shutdown complete");
|
|
698
807
|
process.exit(0);
|
|
699
808
|
} catch (error) {
|
|
700
809
|
logError2("Error during shutdown:", error);
|
|
@@ -702,11 +811,11 @@ process.on("SIGINT", async () => {
|
|
|
702
811
|
}
|
|
703
812
|
});
|
|
704
813
|
process.on("SIGTERM", async () => {
|
|
705
|
-
|
|
706
|
-
|
|
814
|
+
logInfo4("");
|
|
815
|
+
logInfo4("Received SIGTERM, shutting down gracefully...");
|
|
707
816
|
try {
|
|
708
817
|
await service.stop();
|
|
709
|
-
|
|
818
|
+
logInfo4("Shutdown complete");
|
|
710
819
|
process.exit(0);
|
|
711
820
|
} catch (error) {
|
|
712
821
|
logError2("Error during shutdown:", error);
|
|
@@ -724,21 +833,21 @@ setInterval(() => {
|
|
|
724
833
|
}
|
|
725
834
|
}, 3e4);
|
|
726
835
|
if (args.includes("--help") || args.includes("-h")) {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
836
|
+
logInfo4("Usage: usbmuxd-instance-manager [OPTIONS]");
|
|
837
|
+
logInfo4("");
|
|
838
|
+
logInfo4("Options:");
|
|
839
|
+
logInfo4(" --batchSize <n> Number of devices per instance (default: 4)");
|
|
840
|
+
logInfo4(" --basePort <port> Base TCP port for first instance (default: 27015)");
|
|
841
|
+
logInfo4(" --maxInstances <n> Maximum number of instances (default: 20)");
|
|
842
|
+
logInfo4(" --usbmuxdPath <path> Path to usbmuxd executable");
|
|
843
|
+
logInfo4(" --verbose <true|false> Enable verbose logging (default: true)");
|
|
844
|
+
logInfo4(" --appleVid <vid> Apple Vendor ID in hex (default: 05AC)");
|
|
845
|
+
logInfo4(" -h, --help Show this help message");
|
|
846
|
+
logInfo4("");
|
|
847
|
+
logInfo4("Examples:");
|
|
848
|
+
logInfo4(" usbmuxd-instance-manager");
|
|
849
|
+
logInfo4(" usbmuxd-instance-manager --batchSize 6 --basePort 27020");
|
|
850
|
+
logInfo4("");
|
|
742
851
|
process.exit(0);
|
|
743
852
|
}
|
|
744
853
|
try {
|