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