@mcesystems/usbmuxd-instance-manager 1.0.72 → 1.0.74
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 +77 -1
- package/dist/cli.js +974 -350
- package/dist/cli.js.map +4 -4
- package/dist/cli.mjs +1002 -350
- package/dist/cli.mjs.map +4 -4
- package/dist/index.js +541 -313
- package/dist/index.js.map +4 -4
- package/dist/index.mjs +541 -313
- package/dist/index.mjs.map +4 -4
- package/dist/types/InstanceManager.d.ts +19 -43
- 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/UsbmuxdService.d.ts +0 -10
- package/dist/types/UsbmuxdService.d.ts.map +1 -1
- package/dist/types/WindowsPrerequisites.d.ts +21 -0
- package/dist/types/WindowsPrerequisites.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/usbipd.d.ts +11 -0
- package/dist/types/types/usbipd.d.ts.map +1 -0
- package/dist/types/usbipd.d.ts +26 -0
- package/dist/types/usbipd.d.ts.map +1 -0
- package/dist/types/wsl.d.ts +70 -0
- package/dist/types/wsl.d.ts.map +1 -0
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -37,138 +37,114 @@ module.exports = __toCommonJS(index_exports);
|
|
|
37
37
|
|
|
38
38
|
// src/UsbmuxdService.ts
|
|
39
39
|
var import_node_events2 = require("node:events");
|
|
40
|
-
var
|
|
40
|
+
var import_tool_debug_g44 = 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");
|
|
45
44
|
var import_node_events = require("node:events");
|
|
45
|
+
var import_tool_debug_g43 = require("@mcesystems/tool-debug-g4");
|
|
46
|
+
|
|
47
|
+
// src/usbipd.ts
|
|
48
|
+
var import_node_child_process = require("node:child_process");
|
|
46
49
|
var import_node_util = require("node:util");
|
|
47
50
|
var import_tool_debug_g4 = require("@mcesystems/tool-debug-g4");
|
|
48
|
-
var { logInfo, logWarning } = (0, import_tool_debug_g4.createLoggers)("
|
|
51
|
+
var { logInfo, logWarning } = (0, import_tool_debug_g4.createLoggers)("usbipd");
|
|
49
52
|
var execAsync = (0, import_node_util.promisify)(import_node_child_process.exec);
|
|
50
|
-
var
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
});
|
|
53
|
+
var Usbipd = class {
|
|
54
|
+
constructor(usbipdPath) {
|
|
55
|
+
this.usbipdPath = usbipdPath;
|
|
56
|
+
}
|
|
57
|
+
async unbindAllDevicesFromWsl() {
|
|
58
|
+
try {
|
|
59
|
+
await execAsync(`"${this.usbipdPath}" unbind -a`);
|
|
60
|
+
logInfo("All devices unbound from WSL");
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logWarning(`Failed to unbind all devices from WSL: ${error}`);
|
|
68
63
|
}
|
|
69
64
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 };
|
|
65
|
+
async detachAllDevicesFromWsl() {
|
|
66
|
+
try {
|
|
67
|
+
await execAsync(`"${this.usbipdPath}" detach -a`);
|
|
68
|
+
logInfo("All devices detached from WSL");
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logWarning(`Failed to detach all devices from WSL: ${error}`);
|
|
71
|
+
}
|
|
98
72
|
}
|
|
99
73
|
/**
|
|
100
|
-
*
|
|
101
|
-
* This IP is needed to connect from Windows to services inside WSL
|
|
74
|
+
* Detach a device from WSL via usbipd
|
|
102
75
|
*/
|
|
103
|
-
async
|
|
104
|
-
if (this.wslIpAddress) {
|
|
105
|
-
return this.wslIpAddress;
|
|
106
|
-
}
|
|
107
|
-
const distro = this.config.wslDistribution || "alpine-usbmuxd-build";
|
|
76
|
+
async detachDeviceFromWsl(busId) {
|
|
108
77
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (match) {
|
|
112
|
-
this.wslIpAddress = match[1];
|
|
113
|
-
logInfo(`Detected WSL IP address: ${this.wslIpAddress}`);
|
|
114
|
-
return this.wslIpAddress;
|
|
115
|
-
}
|
|
78
|
+
await execAsync(`"${this.usbipdPath}" detach --busid=${busId}`);
|
|
79
|
+
logInfo(`Device ${busId} detached from WSL`);
|
|
116
80
|
} catch (error) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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;
|
|
81
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
+
if (message.includes("no device with busid") || message.includes("There is no device")) {
|
|
83
|
+
logInfo(`Device ${busId} already detached`);
|
|
84
|
+
} else {
|
|
85
|
+
logWarning(`Failed to detach device ${busId}: ${error}`);
|
|
126
86
|
}
|
|
127
|
-
} catch (error) {
|
|
128
|
-
logWarning(`Failed to detect WSL IP via hostname: ${error}`);
|
|
129
87
|
}
|
|
130
|
-
logWarning("Could not detect WSL IP, falling back to localhost");
|
|
131
|
-
this.wslIpAddress = "127.0.0.1";
|
|
132
|
-
return this.wslIpAddress;
|
|
133
88
|
}
|
|
134
89
|
/**
|
|
135
|
-
*
|
|
90
|
+
* Attach a device to WSL via usbipd
|
|
91
|
+
* Note: This requires administrator privileges
|
|
136
92
|
*/
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
93
|
+
async attachDeviceToWsl(busId, wsl, distro) {
|
|
94
|
+
try {
|
|
95
|
+
await wsl.ensureWslRunning();
|
|
96
|
+
logInfo(`Binding device ${busId}...`);
|
|
97
|
+
await execAsync(`"${this.usbipdPath}" bind --busid ${busId} --force`);
|
|
98
|
+
if (distro) {
|
|
99
|
+
logInfo(`Attaching device ${busId} to WSL distribution ${distro}...`);
|
|
100
|
+
await execAsync(`"${this.usbipdPath}" attach --wsl=${distro} --busid=${busId}`);
|
|
101
|
+
} else {
|
|
102
|
+
logInfo(`Attaching device ${busId} to default WSL...`);
|
|
103
|
+
await execAsync(`"${this.usbipdPath}" attach --wsl --busid=${busId}`);
|
|
104
|
+
}
|
|
105
|
+
logInfo(`Device ${busId} attached to WSL successfully`);
|
|
106
|
+
return true;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
if (message.includes("already attached to a client")) {
|
|
110
|
+
logInfo(`Device ${busId} is already attached to WSL, continuing`);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
logWarning(`Failed to attach device ${busId} to WSL: ${error}`);
|
|
114
|
+
return false;
|
|
140
115
|
}
|
|
141
|
-
this.isRunning = true;
|
|
142
|
-
this.startedAt = /* @__PURE__ */ new Date();
|
|
143
|
-
this.emit("started");
|
|
144
116
|
}
|
|
145
117
|
/**
|
|
146
|
-
*
|
|
118
|
+
* Parse usbipd list output to extract device information
|
|
147
119
|
*/
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
120
|
+
parseUsbipdList(output) {
|
|
121
|
+
const devices = [];
|
|
122
|
+
const lines = output.split(/\r?\n/);
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
const match = line.match(/^(\d+-\d+)\s+([0-9a-f]{4}):([0-9a-f]{4})\s+(.+)$/i);
|
|
125
|
+
if (match) {
|
|
126
|
+
const rest = match[4].trim();
|
|
127
|
+
const stateMatch = rest.match(/^(.+?)\s{2,}(\S.*)$/);
|
|
128
|
+
const description = stateMatch ? stateMatch[1].trim() : rest;
|
|
129
|
+
const state = stateMatch ? stateMatch[2].trim() : "Unknown";
|
|
130
|
+
devices.push({
|
|
131
|
+
busId: match[1],
|
|
132
|
+
vid: match[2].toUpperCase(),
|
|
133
|
+
pid: match[3].toUpperCase(),
|
|
134
|
+
description,
|
|
135
|
+
state
|
|
136
|
+
});
|
|
137
|
+
}
|
|
151
138
|
}
|
|
152
|
-
|
|
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");
|
|
139
|
+
return devices;
|
|
164
140
|
}
|
|
165
141
|
/**
|
|
166
142
|
* Find the usbipd bus ID for a device by matching VID/PID
|
|
167
143
|
*/
|
|
168
144
|
async findBusIdForDevice(device) {
|
|
169
145
|
try {
|
|
170
|
-
const { stdout } = await execAsync(
|
|
171
|
-
const usbipdDevices = parseUsbipdList(stdout);
|
|
146
|
+
const { stdout } = await execAsync(`"${this.usbipdPath}" list`);
|
|
147
|
+
const usbipdDevices = this.parseUsbipdList(stdout);
|
|
172
148
|
const deviceVid = device.vid.toString(16).toUpperCase().padStart(4, "0");
|
|
173
149
|
const devicePid = device.pid.toString(16).toUpperCase().padStart(4, "0");
|
|
174
150
|
logInfo(`Looking for device with VID:PID ${deviceVid}:${devicePid}`);
|
|
@@ -180,100 +156,474 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
180
156
|
);
|
|
181
157
|
return match.busId;
|
|
182
158
|
}
|
|
183
|
-
|
|
184
|
-
logInfo(` usbipd device: ${d.busId} ${d.vid}:${d.pid} "${d.description}"`);
|
|
185
|
-
}
|
|
186
|
-
logWarning(
|
|
159
|
+
throw new Error(
|
|
187
160
|
`Could not find usbipd bus ID for device ${device.deviceId} (${deviceVid}:${devicePid})`
|
|
188
161
|
);
|
|
189
|
-
return null;
|
|
190
162
|
} catch (error) {
|
|
191
|
-
|
|
192
|
-
|
|
163
|
+
throw new Error(`Failed to run usbipd list: ${error}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// src/wsl.ts
|
|
169
|
+
var import_node_child_process2 = require("node:child_process");
|
|
170
|
+
var import_node_fs = require("node:fs");
|
|
171
|
+
var import_node_path = require("node:path");
|
|
172
|
+
var import_node_util2 = require("node:util");
|
|
173
|
+
var import_tool_debug_g42 = require("@mcesystems/tool-debug-g4");
|
|
174
|
+
var { logInfo: logInfo2, logWarning: logWarning2, logDetail } = (0, import_tool_debug_g42.createLoggers)("wsl");
|
|
175
|
+
var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.exec);
|
|
176
|
+
var Wsl2 = class {
|
|
177
|
+
constructor(wslDistribution) {
|
|
178
|
+
this.wslDistribution = wslDistribution;
|
|
179
|
+
}
|
|
180
|
+
ALPINE_LOCKDOWN_DIR = "/var/lib/lockdown";
|
|
181
|
+
SYSTEM_CONFIG_PLIST = "SystemConfiguration.plist";
|
|
182
|
+
wslIpAddress = null;
|
|
183
|
+
/**
|
|
184
|
+
* Detect the WSL2 IP address for the configured distribution
|
|
185
|
+
* This IP is needed to connect from Windows to services inside WSL
|
|
186
|
+
*/
|
|
187
|
+
async detectWslIpAddress() {
|
|
188
|
+
if (this.wslIpAddress) {
|
|
189
|
+
return this.wslIpAddress;
|
|
190
|
+
}
|
|
191
|
+
const distro = this.wslDistribution || "alpine-usbmuxd-build";
|
|
192
|
+
try {
|
|
193
|
+
const { stdout } = await execAsync2(`wsl -d ${distro} -- ip -4 addr show eth0`);
|
|
194
|
+
const match = stdout.match(/inet\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
195
|
+
if (match) {
|
|
196
|
+
this.wslIpAddress = match[1];
|
|
197
|
+
logInfo2(`Detected WSL IP address: ${this.wslIpAddress}`);
|
|
198
|
+
return this.wslIpAddress;
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
logWarning2(`Failed to detect WSL IP via ip addr: ${error}`);
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const { stdout } = await execAsync2(`wsl -d ${distro} -- hostname -I`);
|
|
205
|
+
const ip = stdout.trim().split(/\s+/)[0];
|
|
206
|
+
if (ip && /^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
|
|
207
|
+
this.wslIpAddress = ip;
|
|
208
|
+
logInfo2(`Detected WSL IP address (hostname): ${this.wslIpAddress}`);
|
|
209
|
+
return this.wslIpAddress;
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
logWarning2(`Failed to detect WSL IP via hostname: ${error}`);
|
|
193
213
|
}
|
|
214
|
+
logWarning2("Could not detect WSL IP, falling back to localhost");
|
|
215
|
+
this.wslIpAddress = "127.0.0.1";
|
|
216
|
+
return this.wslIpAddress;
|
|
194
217
|
}
|
|
195
218
|
/**
|
|
196
219
|
* Ensure the WSL distribution is running
|
|
197
220
|
*/
|
|
198
221
|
async ensureWslRunning() {
|
|
199
|
-
const distro = this.
|
|
222
|
+
const distro = this.wslDistribution;
|
|
200
223
|
try {
|
|
201
224
|
if (distro) {
|
|
202
|
-
|
|
203
|
-
await
|
|
225
|
+
logInfo2(`Starting WSL distribution: ${distro}...`);
|
|
226
|
+
await execAsync2(`wsl -d ${distro} -- echo "WSL started"`);
|
|
204
227
|
} else {
|
|
205
|
-
|
|
206
|
-
await
|
|
228
|
+
logInfo2("Starting default WSL distribution...");
|
|
229
|
+
await execAsync2(`wsl -- echo "WSL started"`);
|
|
207
230
|
}
|
|
208
231
|
return true;
|
|
209
232
|
} catch (error) {
|
|
210
|
-
|
|
233
|
+
logWarning2(`Failed to start WSL: ${error}`);
|
|
211
234
|
return false;
|
|
212
235
|
}
|
|
213
236
|
}
|
|
214
237
|
/**
|
|
215
|
-
*
|
|
216
|
-
* Note: This requires administrator privileges
|
|
238
|
+
* Create a new usbmuxd instance
|
|
217
239
|
*/
|
|
218
|
-
async
|
|
219
|
-
|
|
240
|
+
async createInstance({
|
|
241
|
+
id,
|
|
242
|
+
basePort,
|
|
243
|
+
verboseLogging,
|
|
244
|
+
usbmuxdPath,
|
|
245
|
+
onLog,
|
|
246
|
+
onExit
|
|
247
|
+
}) {
|
|
248
|
+
const port = basePort + id - 1;
|
|
249
|
+
logInfo2(`Creating instance ${id} on port ${port}`);
|
|
250
|
+
const host = await this.detectWslIpAddress();
|
|
251
|
+
const usbmuxdArgs = [
|
|
252
|
+
"-f",
|
|
253
|
+
// Foreground
|
|
254
|
+
"-v",
|
|
255
|
+
// Verbose (if enabled)
|
|
256
|
+
"-S",
|
|
257
|
+
`0.0.0.0:${port}`,
|
|
258
|
+
// Listen on all interfaces (for Windows → WSL2)
|
|
259
|
+
"--pidfile",
|
|
260
|
+
"NONE"
|
|
261
|
+
];
|
|
262
|
+
if (!verboseLogging) {
|
|
263
|
+
usbmuxdArgs.splice(1, 1);
|
|
264
|
+
}
|
|
265
|
+
const wslArgs = [
|
|
266
|
+
"-d",
|
|
267
|
+
this.wslDistribution || "alpine-usbmuxd-build",
|
|
268
|
+
usbmuxdPath,
|
|
269
|
+
...usbmuxdArgs
|
|
270
|
+
];
|
|
271
|
+
const process2 = (0, import_node_child_process2.spawn)("wsl", wslArgs, {
|
|
272
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
273
|
+
detached: true,
|
|
274
|
+
windowsHide: false
|
|
275
|
+
});
|
|
276
|
+
process2.stdout?.on("data", (data) => {
|
|
277
|
+
onLog(data.toString().trim());
|
|
278
|
+
});
|
|
279
|
+
process2.stderr?.on("data", (data) => {
|
|
280
|
+
onLog(data.toString().trim());
|
|
281
|
+
});
|
|
282
|
+
process2.on("exit", onExit);
|
|
283
|
+
const pid = process2.pid;
|
|
284
|
+
if (pid === void 0) {
|
|
285
|
+
process2.kill("SIGKILL");
|
|
286
|
+
throw new Error("Failed to get PID for usbmuxd instance");
|
|
287
|
+
}
|
|
288
|
+
const instance = {
|
|
289
|
+
id,
|
|
290
|
+
host,
|
|
291
|
+
port,
|
|
292
|
+
pid,
|
|
293
|
+
deviceUdids: [],
|
|
294
|
+
startedAt: /* @__PURE__ */ new Date()
|
|
295
|
+
};
|
|
296
|
+
return { instance, process: process2 };
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Convert a Windows path to the equivalent path inside WSL (e.g. C:\foo\bar -> /mnt/c/foo/bar).
|
|
300
|
+
*/
|
|
301
|
+
windowsPathToWsl(windowsPath) {
|
|
302
|
+
const normalized = windowsPath.replace(/\\/g, "/").trim();
|
|
303
|
+
const driveMatch = normalized.match(/^([a-zA-Z]):\/?(.*)$/);
|
|
304
|
+
if (driveMatch) {
|
|
305
|
+
const drive = driveMatch[1].toLowerCase();
|
|
306
|
+
const rest = driveMatch[2] || "";
|
|
307
|
+
return `/mnt/${drive}${rest ? `/${rest}` : ""}`;
|
|
308
|
+
}
|
|
309
|
+
return normalized;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Get the native Apple lockdown directory path for the current platform.
|
|
313
|
+
* This is where Apple/iTunes stores pairing records natively.
|
|
314
|
+
*/
|
|
315
|
+
getAppleLockdownPath() {
|
|
316
|
+
if (process.platform === "win32") {
|
|
317
|
+
return (0, import_node_path.join)(process.env.ProgramData ?? "C:\\ProgramData", "Apple", "Lockdown");
|
|
318
|
+
}
|
|
319
|
+
if (process.platform === "darwin") {
|
|
320
|
+
return "/var/db/lockdown";
|
|
321
|
+
}
|
|
322
|
+
if (process.platform === "linux") {
|
|
323
|
+
return "/var/lib/lockdown";
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Copy a single device's lockdown plist from the native Apple lockdown directory
|
|
329
|
+
* to Alpine so usbmuxd can use it (skip pairing).
|
|
330
|
+
*
|
|
331
|
+
* Uses the platform-specific Apple/iTunes lockdown directory
|
|
332
|
+
* (e.g. C:\ProgramData\Apple\Lockdown on Windows, /var/db/lockdown on macOS).
|
|
333
|
+
*
|
|
334
|
+
* No-op if the lockdown directory cannot be determined or files don't exist.
|
|
335
|
+
*/
|
|
336
|
+
async syncToAlpine(udid) {
|
|
337
|
+
const lockdownDir = this.getAppleLockdownPath();
|
|
338
|
+
if (!lockdownDir) {
|
|
339
|
+
logWarning2("Lockdown sync to Alpine: unsupported platform, skipping");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
logDetail(`Lockdown sync to Alpine: source dir = ${lockdownDir}, udid = ${udid}`);
|
|
343
|
+
const distro = this.wslDistribution || "alpine-usbmuxd-build";
|
|
344
|
+
const wslDestDir = this.ALPINE_LOCKDOWN_DIR;
|
|
345
|
+
const devicePlistFile = `${udid}.plist`;
|
|
346
|
+
const devicePlistPath = (0, import_node_path.join)(lockdownDir, devicePlistFile);
|
|
347
|
+
if ((0, import_node_fs.existsSync)(devicePlistPath)) {
|
|
348
|
+
await this.copyFileToAlpine(
|
|
349
|
+
this.windowsPathToWsl(devicePlistPath),
|
|
350
|
+
wslDestDir,
|
|
351
|
+
distro,
|
|
352
|
+
devicePlistFile
|
|
353
|
+
);
|
|
354
|
+
} else {
|
|
355
|
+
logDetail(
|
|
356
|
+
`Lockdown sync to Alpine: ${devicePlistFile} not found at ${devicePlistPath}, skipping`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
const systemConfigPath = (0, import_node_path.join)(lockdownDir, this.SYSTEM_CONFIG_PLIST);
|
|
360
|
+
if ((0, import_node_fs.existsSync)(systemConfigPath)) {
|
|
361
|
+
await this.copyFileToAlpine(
|
|
362
|
+
this.windowsPathToWsl(systemConfigPath),
|
|
363
|
+
wslDestDir,
|
|
364
|
+
distro,
|
|
365
|
+
this.SYSTEM_CONFIG_PLIST
|
|
366
|
+
);
|
|
367
|
+
} else {
|
|
368
|
+
logDetail(
|
|
369
|
+
`Lockdown sync to Alpine: ${this.SYSTEM_CONFIG_PLIST} not found at ${systemConfigPath}, skipping`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Copy a single file into the Alpine lockdown directory.
|
|
375
|
+
* Falls back to sudo if the initial copy fails.
|
|
376
|
+
*/
|
|
377
|
+
async copyFileToAlpine(wslSource, wslDestDir, distro, fileName) {
|
|
220
378
|
try {
|
|
221
|
-
await
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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;
|
|
379
|
+
await execAsync2(
|
|
380
|
+
`wsl -d ${distro} -- sh -c "mkdir -p ${wslDestDir} && cp '${wslSource}' '${wslDestDir}/'"`
|
|
381
|
+
);
|
|
382
|
+
logInfo2(`Lockdown sync: copied ${fileName} to Alpine`);
|
|
233
383
|
} catch (error) {
|
|
234
|
-
|
|
235
|
-
|
|
384
|
+
try {
|
|
385
|
+
await execAsync2(
|
|
386
|
+
`wsl -d ${distro} -- sh -c "mkdir -p ${wslDestDir} && sudo cp '${wslSource}' '${wslDestDir}/'"`
|
|
387
|
+
);
|
|
388
|
+
logInfo2(`Lockdown sync: copied ${fileName} to Alpine (via sudo)`);
|
|
389
|
+
} catch (sudoError) {
|
|
390
|
+
logWarning2(
|
|
391
|
+
`Lockdown sync to Alpine failed for ${fileName}: ${error}. Sudo fallback failed: ${sudoError}. Ensure /var/lib/lockdown is writable or use passwordless sudo.`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
236
394
|
}
|
|
237
395
|
}
|
|
238
396
|
/**
|
|
239
|
-
*
|
|
397
|
+
* Copy lockdown plists from Alpine back to the native Apple lockdown directory
|
|
398
|
+
* (for devices that were assigned this session).
|
|
399
|
+
* Also copies SystemConfiguration.plist if present in Alpine.
|
|
400
|
+
*
|
|
401
|
+
* No-op if the lockdown directory cannot be determined.
|
|
240
402
|
*/
|
|
241
|
-
async
|
|
403
|
+
async syncFromAlpine(udids) {
|
|
404
|
+
logInfo2(`Lockdown sync from Alpine: starting (${udids.length} device(s))`);
|
|
405
|
+
const lockdownDir = this.getAppleLockdownPath();
|
|
406
|
+
if (!lockdownDir) {
|
|
407
|
+
logWarning2("Lockdown sync: unsupported platform, skipping");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
logDetail(`Lockdown sync: target dir = ${lockdownDir}`);
|
|
242
411
|
try {
|
|
243
|
-
|
|
244
|
-
|
|
412
|
+
(0, import_node_fs.mkdirSync)(lockdownDir, { recursive: true });
|
|
413
|
+
} catch (error) {
|
|
414
|
+
logWarning2(`Lockdown sync: could not create lockdown dir ${lockdownDir}: ${error}`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const distro = this.wslDistribution || "alpine-usbmuxd-build";
|
|
418
|
+
const wslDestDir = this.windowsPathToWsl(lockdownDir);
|
|
419
|
+
try {
|
|
420
|
+
const { stdout } = await execAsync2(`wsl -d ${distro} -- ls -la ${this.ALPINE_LOCKDOWN_DIR}/`);
|
|
421
|
+
logDetail(`Lockdown sync: Alpine ${this.ALPINE_LOCKDOWN_DIR} contents:
|
|
422
|
+
${stdout}`);
|
|
245
423
|
} catch (error) {
|
|
246
|
-
|
|
424
|
+
logWarning2(`Lockdown sync: could not list Alpine lockdown dir: ${error}`);
|
|
425
|
+
}
|
|
426
|
+
const filesToSync = [...udids.map((udid) => `${udid}.plist`), this.SYSTEM_CONFIG_PLIST];
|
|
427
|
+
logDetail(`Lockdown sync: files to sync = [${filesToSync.join(", ")}]`);
|
|
428
|
+
for (const fileName of filesToSync) {
|
|
429
|
+
await this.copyFileFromAlpine(fileName, wslDestDir, distro, lockdownDir);
|
|
247
430
|
}
|
|
431
|
+
logInfo2("Lockdown sync from Alpine: done");
|
|
248
432
|
}
|
|
249
433
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
434
|
+
* Copy a single file from the Alpine lockdown directory back to Windows.
|
|
435
|
+
* Skips if the file does not exist in Alpine; logs a warning if the copy fails.
|
|
252
436
|
*/
|
|
253
|
-
async
|
|
254
|
-
|
|
255
|
-
|
|
437
|
+
async copyFileFromAlpine(fileName, wslDestDir, distro, lockdownDir) {
|
|
438
|
+
const alpinePath = `${this.ALPINE_LOCKDOWN_DIR}/${fileName}`;
|
|
439
|
+
try {
|
|
440
|
+
await execAsync2(`wsl -d ${distro} -- test -f '${alpinePath}'`);
|
|
441
|
+
} catch {
|
|
442
|
+
logDetail(`Lockdown sync: ${fileName} not found in Alpine (${alpinePath}), skipping`);
|
|
256
443
|
return;
|
|
257
444
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
445
|
+
logDetail(`Lockdown sync: copying ${fileName} \u2192 ${wslDestDir}/`);
|
|
446
|
+
try {
|
|
447
|
+
await execAsync2(`wsl -d ${distro} -- cp -f '${alpinePath}' '${wslDestDir}/'`);
|
|
448
|
+
logInfo2(`Lockdown sync: copied ${fileName} from Alpine to ${lockdownDir}`);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
logWarning2(`Lockdown sync: failed to copy ${fileName} from Alpine to ${lockdownDir}: ${error}`);
|
|
261
451
|
}
|
|
262
|
-
|
|
263
|
-
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// src/InstanceManager.ts
|
|
456
|
+
var { logWarning: logWarning3, logDetail: logDetail2, logDataObject, logTask } = (0, import_tool_debug_g43.createLoggers)("instance-manager");
|
|
457
|
+
var DEFAULT_CONFIG = {
|
|
458
|
+
batchSize: 4,
|
|
459
|
+
basePort: 27015,
|
|
460
|
+
maxInstances: 5,
|
|
461
|
+
usbmuxdPath: "usbmuxd",
|
|
462
|
+
// Path inside WSL2
|
|
463
|
+
wslDistribution: "alpine-usbmuxd-build",
|
|
464
|
+
// Alpine WSL2 distribution name
|
|
465
|
+
verboseLogging: true,
|
|
466
|
+
appleVendorId: "05AC"
|
|
467
|
+
};
|
|
468
|
+
var InstanceManager = class extends import_node_events.EventEmitter {
|
|
469
|
+
/** Map of usbmuxd instance IDs to instances */
|
|
470
|
+
instances = /* @__PURE__ */ new Map();
|
|
471
|
+
/** Map of device IDs to device mappings host:port */
|
|
472
|
+
deviceMappings = /* @__PURE__ */ new Map();
|
|
473
|
+
/** Map of usbmuxd instance IDs to child processes running it on wsl2*/
|
|
474
|
+
processes = /* @__PURE__ */ new Map();
|
|
475
|
+
/** Tracks which devices have been attached to WSL */
|
|
476
|
+
attachedDevices = /* @__PURE__ */ new Set();
|
|
477
|
+
/** Device IDs currently in the attach flow
|
|
478
|
+
* (ignore disconnect until attach completes) */
|
|
479
|
+
pendingAttachDevices = /* @__PURE__ */ new Set();
|
|
480
|
+
config;
|
|
481
|
+
nextInstanceId = 1;
|
|
482
|
+
startedAt = null;
|
|
483
|
+
isRunning = false;
|
|
484
|
+
wsl;
|
|
485
|
+
usbipd;
|
|
486
|
+
constructor(config = {}) {
|
|
487
|
+
super();
|
|
488
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
489
|
+
this.wsl = new Wsl2(this.config.wslDistribution);
|
|
490
|
+
this.usbipd = new Usbipd(process.env.USBIPD_PATH ?? "usbipd");
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Start the instance manager
|
|
494
|
+
*/
|
|
495
|
+
start() {
|
|
496
|
+
if (this.isRunning) {
|
|
497
|
+
throw new Error("Instance manager is already running");
|
|
498
|
+
}
|
|
499
|
+
this.isRunning = true;
|
|
500
|
+
this.startedAt = /* @__PURE__ */ new Date();
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Stop the instance manager and all instances
|
|
504
|
+
*/
|
|
505
|
+
async stop() {
|
|
506
|
+
if (!this.isRunning) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
this.isRunning = false;
|
|
510
|
+
const udids = Array.from(this.deviceMappings.keys());
|
|
511
|
+
await this.wsl.syncFromAlpine(udids);
|
|
512
|
+
await this.usbipd.detachAllDevicesFromWsl();
|
|
513
|
+
await this.usbipd.unbindAllDevicesFromWsl();
|
|
514
|
+
this.attachedDevices.clear();
|
|
515
|
+
const stopPromises = Array.from(this.instances.keys()).map((id) => this.stopInstance(id));
|
|
516
|
+
await Promise.all(stopPromises);
|
|
517
|
+
this.instances.clear();
|
|
518
|
+
this.deviceMappings.clear();
|
|
519
|
+
this.processes.clear();
|
|
520
|
+
}
|
|
521
|
+
async attachToWsl({
|
|
522
|
+
busId,
|
|
523
|
+
deviceId
|
|
524
|
+
}) {
|
|
525
|
+
if (!this.attachedDevices.has(busId)) {
|
|
526
|
+
this.pendingAttachDevices.add(deviceId);
|
|
527
|
+
if (!this.config.wslDistribution) {
|
|
528
|
+
throw new Error("WSL distribution not configured");
|
|
529
|
+
}
|
|
530
|
+
const attached = await this.usbipd.attachDeviceToWsl(
|
|
531
|
+
busId,
|
|
532
|
+
this.wsl,
|
|
533
|
+
this.config.wslDistribution
|
|
534
|
+
);
|
|
264
535
|
if (attached) {
|
|
265
536
|
this.attachedDevices.add(busId);
|
|
266
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
267
537
|
}
|
|
268
538
|
}
|
|
539
|
+
}
|
|
540
|
+
async removeDevice(mapping) {
|
|
541
|
+
const instance = await this.detachFromWsl({ deviceId: mapping.udid });
|
|
542
|
+
if (!instance) {
|
|
543
|
+
throw new Error(`Instance ${mapping.instanceId} not found`);
|
|
544
|
+
}
|
|
545
|
+
if (mapping.busId) {
|
|
546
|
+
await this.usbipd.detachDeviceFromWsl(mapping.busId);
|
|
547
|
+
this.attachedDevices.delete(mapping.busId);
|
|
548
|
+
}
|
|
549
|
+
const deviceIndex = instance.deviceUdids.indexOf(mapping.udid);
|
|
550
|
+
if (deviceIndex > -1) {
|
|
551
|
+
instance.deviceUdids.splice(deviceIndex, 1);
|
|
552
|
+
}
|
|
553
|
+
this.deviceMappings.delete(mapping.udid);
|
|
554
|
+
return instance;
|
|
555
|
+
}
|
|
556
|
+
async onUsbmuxdInstanceEnd({
|
|
557
|
+
instanceId,
|
|
558
|
+
code,
|
|
559
|
+
signal
|
|
560
|
+
}) {
|
|
561
|
+
this.emit("instance-exited", {
|
|
562
|
+
instanceId,
|
|
563
|
+
code,
|
|
564
|
+
signal
|
|
565
|
+
});
|
|
566
|
+
for (const [_, mapping] of this.deviceMappings.entries()) {
|
|
567
|
+
if (mapping.instanceId === instanceId) {
|
|
568
|
+
this.removeDevice(mapping);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
this.instances.delete(instanceId);
|
|
572
|
+
this.processes.get(instanceId)?.kill();
|
|
573
|
+
this.processes.delete(instanceId);
|
|
574
|
+
}
|
|
575
|
+
async onUsbmuxdInstanceStart({
|
|
576
|
+
instance,
|
|
577
|
+
process: process2
|
|
578
|
+
}) {
|
|
579
|
+
this.instances.set(instance.id, instance);
|
|
580
|
+
this.processes.set(instance.id, process2);
|
|
581
|
+
this.emit("instance-started", {
|
|
582
|
+
instanceId: instance.id
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
async getInstance() {
|
|
269
586
|
let targetInstance = this.findInstanceWithCapacity();
|
|
270
587
|
if (!targetInstance) {
|
|
271
588
|
if (this.instances.size >= this.config.maxInstances) {
|
|
272
589
|
throw new Error(`Maximum number of instances (${this.config.maxInstances}) reached`);
|
|
273
590
|
}
|
|
274
|
-
|
|
591
|
+
const newInstanceId = this.nextInstanceId++;
|
|
592
|
+
let onExit = (code, signal) => {
|
|
593
|
+
this.onUsbmuxdInstanceEnd({ instanceId: newInstanceId, code, signal });
|
|
594
|
+
};
|
|
595
|
+
onExit = onExit.bind(this);
|
|
596
|
+
const { instance, process: process2 } = await this.wsl.createInstance({
|
|
597
|
+
id: newInstanceId,
|
|
598
|
+
basePort: this.config.basePort,
|
|
599
|
+
verboseLogging: this.config.verboseLogging,
|
|
600
|
+
usbmuxdPath: this.config.usbmuxdPath,
|
|
601
|
+
onLog: (message) => this.emit("instance-log", {
|
|
602
|
+
instanceId: newInstanceId,
|
|
603
|
+
level: "info",
|
|
604
|
+
message
|
|
605
|
+
}),
|
|
606
|
+
onExit
|
|
607
|
+
});
|
|
608
|
+
this.onUsbmuxdInstanceStart({ instance, process: process2 });
|
|
609
|
+
targetInstance = instance;
|
|
275
610
|
}
|
|
276
|
-
targetInstance
|
|
611
|
+
return targetInstance;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Handle device connection only for not attached devices
|
|
615
|
+
* Attaches device to WSL, then assigns to an existing instance or creates a new one
|
|
616
|
+
*/
|
|
617
|
+
async onDeviceConnected(device) {
|
|
618
|
+
const busId = await this.usbipd.findBusIdForDevice(device);
|
|
619
|
+
if (this.attachedDevices.has(busId)) {
|
|
620
|
+
logTask(`Device ${device.deviceId} is already attached. skipping onDeviceConnected...`);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
logDataObject("Device connected", { device });
|
|
624
|
+
await this.wsl.syncToAlpine(device.deviceId);
|
|
625
|
+
await this.attachToWsl({ busId, deviceId: device.deviceId });
|
|
626
|
+
const targetInstance = await this.getInstance();
|
|
277
627
|
const mapping = {
|
|
278
628
|
udid: device.deviceId,
|
|
279
629
|
instanceId: targetInstance.id,
|
|
@@ -283,74 +633,52 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
283
633
|
busId: busId ?? void 0
|
|
284
634
|
};
|
|
285
635
|
this.deviceMappings.set(device.deviceId, mapping);
|
|
636
|
+
targetInstance.deviceUdids.push(device.deviceId);
|
|
286
637
|
this.emit("device-assigned", {
|
|
287
638
|
device,
|
|
288
639
|
instance: targetInstance,
|
|
289
640
|
mapping
|
|
290
641
|
});
|
|
642
|
+
this.pendingAttachDevices.delete(device.deviceId);
|
|
291
643
|
}
|
|
292
|
-
|
|
293
|
-
|
|
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);
|
|
644
|
+
async detachFromWsl({ deviceId }) {
|
|
645
|
+
const mapping = this.deviceMappings.get(deviceId);
|
|
303
646
|
if (!mapping) {
|
|
304
|
-
|
|
305
|
-
return false;
|
|
647
|
+
throw new Error(`Device ${deviceId} was not tracked`);
|
|
306
648
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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;
|
|
649
|
+
if (mapping.busId) {
|
|
650
|
+
await this.usbipd.detachDeviceFromWsl(mapping.busId);
|
|
651
|
+
this.attachedDevices.delete(mapping.busId);
|
|
652
|
+
}
|
|
653
|
+
const instance = this.instances.get(mapping.instanceId);
|
|
654
|
+
if (!instance) {
|
|
655
|
+
logWarning3(`Instance ${mapping.instanceId} not found`);
|
|
656
|
+
this.deviceMappings.delete(deviceId);
|
|
657
|
+
throw new Error(`Instance ${mapping.instanceId} not found`);
|
|
322
658
|
}
|
|
659
|
+
return instance;
|
|
323
660
|
}
|
|
324
661
|
/**
|
|
325
|
-
* Handle device disconnection
|
|
662
|
+
* Handle device disconnection only for attached devices
|
|
326
663
|
* Detaches from WSL, removes device from instance, and stops instance if empty
|
|
327
664
|
*/
|
|
328
665
|
async onDeviceDisconnected(device) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
logWarning(`Device ${device.deviceId} was not tracked`);
|
|
666
|
+
if (!this.attachedDevices.has(device.deviceId)) {
|
|
667
|
+
logTask(`Device ${device.deviceId} is not attached. skipping onDeviceDisconnected...`);
|
|
332
668
|
return;
|
|
333
669
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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);
|
|
670
|
+
logDataObject("Device disconnected", { device });
|
|
671
|
+
const mapping = this.deviceMappings.get(device.deviceId);
|
|
672
|
+
if (!mapping) {
|
|
673
|
+
throw new Error(`Device ${device.deviceId} was not tracked`);
|
|
347
674
|
}
|
|
348
|
-
this.
|
|
675
|
+
const instance = await this.removeDevice(mapping);
|
|
349
676
|
this.emit("device-removed", {
|
|
350
677
|
device,
|
|
351
678
|
instance
|
|
352
679
|
});
|
|
353
680
|
if (instance.deviceUdids.length === 0) {
|
|
681
|
+
logDetail2(`Instance ${instance.id} is empty, stopping...`);
|
|
354
682
|
await this.stopInstance(instance.id);
|
|
355
683
|
}
|
|
356
684
|
}
|
|
@@ -365,104 +693,18 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
365
693
|
}
|
|
366
694
|
return null;
|
|
367
695
|
}
|
|
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
696
|
/**
|
|
445
697
|
* Stop a specific instance
|
|
446
698
|
*/
|
|
447
699
|
async stopInstance(instanceId) {
|
|
448
700
|
const instance = this.instances.get(instanceId);
|
|
449
|
-
const
|
|
450
|
-
if (!instance || !
|
|
701
|
+
const childProcess = this.processes.get(instanceId);
|
|
702
|
+
if (!instance || !childProcess) {
|
|
451
703
|
return;
|
|
452
704
|
}
|
|
453
|
-
process2.
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (!process2.killed) {
|
|
457
|
-
process2.kill("SIGKILL");
|
|
458
|
-
}
|
|
459
|
-
resolve();
|
|
460
|
-
}, 5e3);
|
|
461
|
-
process2.once("exit", () => {
|
|
462
|
-
clearTimeout(timeout);
|
|
463
|
-
resolve();
|
|
464
|
-
});
|
|
465
|
-
});
|
|
705
|
+
for (const process2 of this.processes.values()) {
|
|
706
|
+
process2.kill("SIGKILL");
|
|
707
|
+
}
|
|
466
708
|
this.instances.delete(instanceId);
|
|
467
709
|
this.processes.delete(instanceId);
|
|
468
710
|
for (const [udid, mapping] of this.deviceMappings.entries()) {
|
|
@@ -514,7 +756,7 @@ var InstanceManager = class extends import_node_events.EventEmitter {
|
|
|
514
756
|
};
|
|
515
757
|
|
|
516
758
|
// src/UsbmuxdService.ts
|
|
517
|
-
var { logInfo:
|
|
759
|
+
var { logInfo: logInfo3, logError, logDataObject: logDataObject2 } = (0, import_tool_debug_g44.createLoggers)("usbmuxd-service");
|
|
518
760
|
var UsbmuxdService = class extends import_node_events2.EventEmitter {
|
|
519
761
|
manager;
|
|
520
762
|
usbListener;
|
|
@@ -538,6 +780,9 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
|
|
|
538
780
|
this.manager.on(
|
|
539
781
|
"device-assigned",
|
|
540
782
|
(payload) => {
|
|
783
|
+
logInfo3(
|
|
784
|
+
`Device ${payload.device.deviceId} connected to usbmuxd instance at ${payload.mapping.host}:${payload.mapping.port}`
|
|
785
|
+
);
|
|
541
786
|
this.emit("device-assigned", payload);
|
|
542
787
|
}
|
|
543
788
|
);
|
|
@@ -573,17 +818,12 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
|
|
|
573
818
|
}
|
|
574
819
|
const config = this.manager.getConfig();
|
|
575
820
|
const appleVid = Number.parseInt(config.appleVendorId, 16);
|
|
576
|
-
|
|
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})`);
|
|
821
|
+
logDataObject2("Starting service...", { config });
|
|
582
822
|
this.usbListener.onDeviceAdd(async (device) => {
|
|
583
823
|
if (device.vid !== appleVid) {
|
|
584
824
|
return;
|
|
585
825
|
}
|
|
586
|
-
|
|
826
|
+
logInfo3(`Apple device connected: ${device.deviceId} (${device.deviceName || "Unknown"})`);
|
|
587
827
|
try {
|
|
588
828
|
await this.manager.onDeviceConnected(device);
|
|
589
829
|
} catch (error) {
|
|
@@ -594,7 +834,7 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
|
|
|
594
834
|
if (device.vid !== appleVid) {
|
|
595
835
|
return;
|
|
596
836
|
}
|
|
597
|
-
|
|
837
|
+
logInfo3(`Apple device disconnected: ${device.deviceId}`);
|
|
598
838
|
try {
|
|
599
839
|
await this.manager.onDeviceDisconnected(device);
|
|
600
840
|
} catch (error) {
|
|
@@ -608,8 +848,8 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
|
|
|
608
848
|
});
|
|
609
849
|
this.isListening = true;
|
|
610
850
|
this.manager.start();
|
|
611
|
-
|
|
612
|
-
|
|
851
|
+
logInfo3("Service started successfully");
|
|
852
|
+
logInfo3("Waiting for iOS devices...");
|
|
613
853
|
} catch (error) {
|
|
614
854
|
logError("Failed to start USB listener:", error);
|
|
615
855
|
throw error;
|
|
@@ -623,11 +863,11 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
|
|
|
623
863
|
if (!this.isListening) {
|
|
624
864
|
return;
|
|
625
865
|
}
|
|
626
|
-
|
|
866
|
+
logInfo3("Stopping service...");
|
|
627
867
|
this.usbListener.stopListening();
|
|
628
868
|
this.isListening = false;
|
|
629
869
|
await this.manager.stop();
|
|
630
|
-
|
|
870
|
+
logInfo3("Service stopped");
|
|
631
871
|
}
|
|
632
872
|
/**
|
|
633
873
|
* Get device port mapping
|
|
@@ -659,18 +899,6 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
|
|
|
659
899
|
getConfig() {
|
|
660
900
|
return this.manager.getConfig();
|
|
661
901
|
}
|
|
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
902
|
};
|
|
675
903
|
// Annotate the CommonJS export names for ESM import in node:
|
|
676
904
|
0 && (module.exports = {
|