@mcesystems/usbmuxd-instance-manager 1.0.73 → 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/dist/cli.mjs CHANGED
@@ -1,191 +1,890 @@
1
1
  #!/usr/bin/env node
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
10
+ }) : x)(function(x) {
11
+ if (typeof require !== "undefined") return require.apply(this, arguments);
12
+ throw Error('Dynamic require of "' + x + '" is not supported');
13
+ });
14
+ var __commonJS = (cb, mod) => function __require2() {
15
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
16
+ };
17
+ var __copyProps = (to, from, except, desc) => {
18
+ if (from && typeof from === "object" || typeof from === "function") {
19
+ for (let key of __getOwnPropNames(from))
20
+ if (!__hasOwnProp.call(to, key) && key !== except)
21
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
22
+ }
23
+ return to;
24
+ };
25
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
26
+ // If the importer is in node compatibility mode or this is not an ESM
27
+ // file that has been converted to a CommonJS file using a Babel-
28
+ // compatible transform (i.e. "__esModule" has not been set), then set
29
+ // "default" to the CommonJS "module.exports" for node compatibility.
30
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
31
+ mod
32
+ ));
33
+
34
+ // ../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/package.json
35
+ var require_package = __commonJS({
36
+ "../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/package.json"(exports, module) {
37
+ module.exports = {
38
+ name: "dotenv",
39
+ version: "17.2.3",
40
+ description: "Loads environment variables from .env file",
41
+ main: "lib/main.js",
42
+ types: "lib/main.d.ts",
43
+ exports: {
44
+ ".": {
45
+ types: "./lib/main.d.ts",
46
+ require: "./lib/main.js",
47
+ default: "./lib/main.js"
48
+ },
49
+ "./config": "./config.js",
50
+ "./config.js": "./config.js",
51
+ "./lib/env-options": "./lib/env-options.js",
52
+ "./lib/env-options.js": "./lib/env-options.js",
53
+ "./lib/cli-options": "./lib/cli-options.js",
54
+ "./lib/cli-options.js": "./lib/cli-options.js",
55
+ "./package.json": "./package.json"
56
+ },
57
+ scripts: {
58
+ "dts-check": "tsc --project tests/types/tsconfig.json",
59
+ lint: "standard",
60
+ pretest: "npm run lint && npm run dts-check",
61
+ test: "tap run tests/**/*.js --allow-empty-coverage --disable-coverage --timeout=60000",
62
+ "test:coverage": "tap run tests/**/*.js --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov",
63
+ prerelease: "npm test",
64
+ release: "standard-version"
65
+ },
66
+ repository: {
67
+ type: "git",
68
+ url: "git://github.com/motdotla/dotenv.git"
69
+ },
70
+ homepage: "https://github.com/motdotla/dotenv#readme",
71
+ funding: "https://dotenvx.com",
72
+ keywords: [
73
+ "dotenv",
74
+ "env",
75
+ ".env",
76
+ "environment",
77
+ "variables",
78
+ "config",
79
+ "settings"
80
+ ],
81
+ readmeFilename: "README.md",
82
+ license: "BSD-2-Clause",
83
+ devDependencies: {
84
+ "@types/node": "^18.11.3",
85
+ decache: "^4.6.2",
86
+ sinon: "^14.0.1",
87
+ standard: "^17.0.0",
88
+ "standard-version": "^9.5.0",
89
+ tap: "^19.2.0",
90
+ typescript: "^4.8.4"
91
+ },
92
+ engines: {
93
+ node: ">=12"
94
+ },
95
+ browser: {
96
+ fs: false
97
+ }
98
+ };
99
+ }
100
+ });
101
+
102
+ // ../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js
103
+ var require_main = __commonJS({
104
+ "../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js"(exports, module) {
105
+ var fs = __require("fs");
106
+ var path = __require("path");
107
+ var os = __require("os");
108
+ var crypto = __require("crypto");
109
+ var packageJson = require_package();
110
+ var version = packageJson.version;
111
+ var TIPS = [
112
+ "\u{1F510} encrypt with Dotenvx: https://dotenvx.com",
113
+ "\u{1F510} prevent committing .env to code: https://dotenvx.com/precommit",
114
+ "\u{1F510} prevent building .env in docker: https://dotenvx.com/prebuild",
115
+ "\u{1F4E1} add observability to secrets: https://dotenvx.com/ops",
116
+ "\u{1F465} sync secrets across teammates & machines: https://dotenvx.com/ops",
117
+ "\u{1F5C2}\uFE0F backup and recover secrets: https://dotenvx.com/ops",
118
+ "\u2705 audit secrets and track compliance: https://dotenvx.com/ops",
119
+ "\u{1F504} add secrets lifecycle management: https://dotenvx.com/ops",
120
+ "\u{1F511} add access controls to secrets: https://dotenvx.com/ops",
121
+ "\u{1F6E0}\uFE0F run anywhere with `dotenvx run -- yourcommand`",
122
+ "\u2699\uFE0F specify custom .env file path with { path: '/custom/path/.env' }",
123
+ "\u2699\uFE0F enable debug logging with { debug: true }",
124
+ "\u2699\uFE0F override existing env vars with { override: true }",
125
+ "\u2699\uFE0F suppress all logs with { quiet: true }",
126
+ "\u2699\uFE0F write to custom object with { processEnv: myObject }",
127
+ "\u2699\uFE0F load multiple .env files with { path: ['.env.local', '.env'] }"
128
+ ];
129
+ function _getRandomTip() {
130
+ return TIPS[Math.floor(Math.random() * TIPS.length)];
131
+ }
132
+ function parseBoolean(value) {
133
+ if (typeof value === "string") {
134
+ return !["false", "0", "no", "off", ""].includes(value.toLowerCase());
135
+ }
136
+ return Boolean(value);
137
+ }
138
+ function supportsAnsi() {
139
+ return process.stdout.isTTY;
140
+ }
141
+ function dim(text) {
142
+ return supportsAnsi() ? `\x1B[2m${text}\x1B[0m` : text;
143
+ }
144
+ var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
145
+ function parse(src) {
146
+ const obj = {};
147
+ let lines = src.toString();
148
+ lines = lines.replace(/\r\n?/mg, "\n");
149
+ let match;
150
+ while ((match = LINE.exec(lines)) != null) {
151
+ const key = match[1];
152
+ let value = match[2] || "";
153
+ value = value.trim();
154
+ const maybeQuote = value[0];
155
+ value = value.replace(/^(['"`])([\s\S]*)\1$/mg, "$2");
156
+ if (maybeQuote === '"') {
157
+ value = value.replace(/\\n/g, "\n");
158
+ value = value.replace(/\\r/g, "\r");
159
+ }
160
+ obj[key] = value;
161
+ }
162
+ return obj;
163
+ }
164
+ function _parseVault(options2) {
165
+ options2 = options2 || {};
166
+ const vaultPath = _vaultPath(options2);
167
+ options2.path = vaultPath;
168
+ const result = DotenvModule.configDotenv(options2);
169
+ if (!result.parsed) {
170
+ const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`);
171
+ err.code = "MISSING_DATA";
172
+ throw err;
173
+ }
174
+ const keys = _dotenvKey(options2).split(",");
175
+ const length = keys.length;
176
+ let decrypted;
177
+ for (let i = 0; i < length; i++) {
178
+ try {
179
+ const key = keys[i].trim();
180
+ const attrs = _instructions(result, key);
181
+ decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
182
+ break;
183
+ } catch (error) {
184
+ if (i + 1 >= length) {
185
+ throw error;
186
+ }
187
+ }
188
+ }
189
+ return DotenvModule.parse(decrypted);
190
+ }
191
+ function _warn(message) {
192
+ console.error(`[dotenv@${version}][WARN] ${message}`);
193
+ }
194
+ function _debug(message) {
195
+ console.log(`[dotenv@${version}][DEBUG] ${message}`);
196
+ }
197
+ function _log(message) {
198
+ console.log(`[dotenv@${version}] ${message}`);
199
+ }
200
+ function _dotenvKey(options2) {
201
+ if (options2 && options2.DOTENV_KEY && options2.DOTENV_KEY.length > 0) {
202
+ return options2.DOTENV_KEY;
203
+ }
204
+ if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
205
+ return process.env.DOTENV_KEY;
206
+ }
207
+ return "";
208
+ }
209
+ function _instructions(result, dotenvKey) {
210
+ let uri;
211
+ try {
212
+ uri = new URL(dotenvKey);
213
+ } catch (error) {
214
+ if (error.code === "ERR_INVALID_URL") {
215
+ const err = new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development");
216
+ err.code = "INVALID_DOTENV_KEY";
217
+ throw err;
218
+ }
219
+ throw error;
220
+ }
221
+ const key = uri.password;
222
+ if (!key) {
223
+ const err = new Error("INVALID_DOTENV_KEY: Missing key part");
224
+ err.code = "INVALID_DOTENV_KEY";
225
+ throw err;
226
+ }
227
+ const environment = uri.searchParams.get("environment");
228
+ if (!environment) {
229
+ const err = new Error("INVALID_DOTENV_KEY: Missing environment part");
230
+ err.code = "INVALID_DOTENV_KEY";
231
+ throw err;
232
+ }
233
+ const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`;
234
+ const ciphertext = result.parsed[environmentKey];
235
+ if (!ciphertext) {
236
+ const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`);
237
+ err.code = "NOT_FOUND_DOTENV_ENVIRONMENT";
238
+ throw err;
239
+ }
240
+ return { ciphertext, key };
241
+ }
242
+ function _vaultPath(options2) {
243
+ let possibleVaultPath = null;
244
+ if (options2 && options2.path && options2.path.length > 0) {
245
+ if (Array.isArray(options2.path)) {
246
+ for (const filepath of options2.path) {
247
+ if (fs.existsSync(filepath)) {
248
+ possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
249
+ }
250
+ }
251
+ } else {
252
+ possibleVaultPath = options2.path.endsWith(".vault") ? options2.path : `${options2.path}.vault`;
253
+ }
254
+ } else {
255
+ possibleVaultPath = path.resolve(process.cwd(), ".env.vault");
256
+ }
257
+ if (fs.existsSync(possibleVaultPath)) {
258
+ return possibleVaultPath;
259
+ }
260
+ return null;
261
+ }
262
+ function _resolveHome(envPath) {
263
+ return envPath[0] === "~" ? path.join(os.homedir(), envPath.slice(1)) : envPath;
264
+ }
265
+ function _configVault(options2) {
266
+ const debug = parseBoolean(process.env.DOTENV_CONFIG_DEBUG || options2 && options2.debug);
267
+ const quiet = parseBoolean(process.env.DOTENV_CONFIG_QUIET || options2 && options2.quiet);
268
+ if (debug || !quiet) {
269
+ _log("Loading env from encrypted .env.vault");
270
+ }
271
+ const parsed = DotenvModule._parseVault(options2);
272
+ let processEnv = process.env;
273
+ if (options2 && options2.processEnv != null) {
274
+ processEnv = options2.processEnv;
275
+ }
276
+ DotenvModule.populate(processEnv, parsed, options2);
277
+ return { parsed };
278
+ }
279
+ function configDotenv(options2) {
280
+ const dotenvPath = path.resolve(process.cwd(), ".env");
281
+ let encoding = "utf8";
282
+ let processEnv = process.env;
283
+ if (options2 && options2.processEnv != null) {
284
+ processEnv = options2.processEnv;
285
+ }
286
+ let debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || options2 && options2.debug);
287
+ let quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || options2 && options2.quiet);
288
+ if (options2 && options2.encoding) {
289
+ encoding = options2.encoding;
290
+ } else {
291
+ if (debug) {
292
+ _debug("No encoding is specified. UTF-8 is used by default");
293
+ }
294
+ }
295
+ let optionPaths = [dotenvPath];
296
+ if (options2 && options2.path) {
297
+ if (!Array.isArray(options2.path)) {
298
+ optionPaths = [_resolveHome(options2.path)];
299
+ } else {
300
+ optionPaths = [];
301
+ for (const filepath of options2.path) {
302
+ optionPaths.push(_resolveHome(filepath));
303
+ }
304
+ }
305
+ }
306
+ let lastError;
307
+ const parsedAll = {};
308
+ for (const path2 of optionPaths) {
309
+ try {
310
+ const parsed = DotenvModule.parse(fs.readFileSync(path2, { encoding }));
311
+ DotenvModule.populate(parsedAll, parsed, options2);
312
+ } catch (e) {
313
+ if (debug) {
314
+ _debug(`Failed to load ${path2} ${e.message}`);
315
+ }
316
+ lastError = e;
317
+ }
318
+ }
319
+ const populated = DotenvModule.populate(processEnv, parsedAll, options2);
320
+ debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || debug);
321
+ quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || quiet);
322
+ if (debug || !quiet) {
323
+ const keysCount = Object.keys(populated).length;
324
+ const shortPaths = [];
325
+ for (const filePath of optionPaths) {
326
+ try {
327
+ const relative = path.relative(process.cwd(), filePath);
328
+ shortPaths.push(relative);
329
+ } catch (e) {
330
+ if (debug) {
331
+ _debug(`Failed to load ${filePath} ${e.message}`);
332
+ }
333
+ lastError = e;
334
+ }
335
+ }
336
+ _log(`injecting env (${keysCount}) from ${shortPaths.join(",")} ${dim(`-- tip: ${_getRandomTip()}`)}`);
337
+ }
338
+ if (lastError) {
339
+ return { parsed: parsedAll, error: lastError };
340
+ } else {
341
+ return { parsed: parsedAll };
342
+ }
343
+ }
344
+ function config2(options2) {
345
+ if (_dotenvKey(options2).length === 0) {
346
+ return DotenvModule.configDotenv(options2);
347
+ }
348
+ const vaultPath = _vaultPath(options2);
349
+ if (!vaultPath) {
350
+ _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`);
351
+ return DotenvModule.configDotenv(options2);
352
+ }
353
+ return DotenvModule._configVault(options2);
354
+ }
355
+ function decrypt(encrypted, keyStr) {
356
+ const key = Buffer.from(keyStr.slice(-64), "hex");
357
+ let ciphertext = Buffer.from(encrypted, "base64");
358
+ const nonce = ciphertext.subarray(0, 12);
359
+ const authTag = ciphertext.subarray(-16);
360
+ ciphertext = ciphertext.subarray(12, -16);
361
+ try {
362
+ const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce);
363
+ aesgcm.setAuthTag(authTag);
364
+ return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
365
+ } catch (error) {
366
+ const isRange = error instanceof RangeError;
367
+ const invalidKeyLength = error.message === "Invalid key length";
368
+ const decryptionFailed = error.message === "Unsupported state or unable to authenticate data";
369
+ if (isRange || invalidKeyLength) {
370
+ const err = new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)");
371
+ err.code = "INVALID_DOTENV_KEY";
372
+ throw err;
373
+ } else if (decryptionFailed) {
374
+ const err = new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY");
375
+ err.code = "DECRYPTION_FAILED";
376
+ throw err;
377
+ } else {
378
+ throw error;
379
+ }
380
+ }
381
+ }
382
+ function populate(processEnv, parsed, options2 = {}) {
383
+ const debug = Boolean(options2 && options2.debug);
384
+ const override = Boolean(options2 && options2.override);
385
+ const populated = {};
386
+ if (typeof parsed !== "object") {
387
+ const err = new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate");
388
+ err.code = "OBJECT_REQUIRED";
389
+ throw err;
390
+ }
391
+ for (const key of Object.keys(parsed)) {
392
+ if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
393
+ if (override === true) {
394
+ processEnv[key] = parsed[key];
395
+ populated[key] = parsed[key];
396
+ }
397
+ if (debug) {
398
+ if (override === true) {
399
+ _debug(`"${key}" is already defined and WAS overwritten`);
400
+ } else {
401
+ _debug(`"${key}" is already defined and was NOT overwritten`);
402
+ }
403
+ }
404
+ } else {
405
+ processEnv[key] = parsed[key];
406
+ populated[key] = parsed[key];
407
+ }
408
+ }
409
+ return populated;
410
+ }
411
+ var DotenvModule = {
412
+ configDotenv,
413
+ _configVault,
414
+ _parseVault,
415
+ config: config2,
416
+ decrypt,
417
+ parse,
418
+ populate
419
+ };
420
+ module.exports.configDotenv = DotenvModule.configDotenv;
421
+ module.exports._configVault = DotenvModule._configVault;
422
+ module.exports._parseVault = DotenvModule._parseVault;
423
+ module.exports.config = DotenvModule.config;
424
+ module.exports.decrypt = DotenvModule.decrypt;
425
+ module.exports.parse = DotenvModule.parse;
426
+ module.exports.populate = DotenvModule.populate;
427
+ module.exports = DotenvModule;
428
+ }
429
+ });
2
430
 
3
431
  // src/cli.ts
4
- import { createLoggers as createLoggers4 } from "@mcesystems/tool-debug-g4";
432
+ var import_dotenv = __toESM(require_main());
433
+ import { createLoggers as createLoggers5 } from "@mcesystems/tool-debug-g4";
5
434
 
6
435
  // src/UsbmuxdService.ts
7
436
  import { EventEmitter as EventEmitter2 } from "node:events";
8
- import { createLoggers as createLoggers3 } from "@mcesystems/tool-debug-g4";
437
+ import { createLoggers as createLoggers4 } from "@mcesystems/tool-debug-g4";
9
438
  import UsbDeviceListener from "@mcesystems/usb-device-listener";
10
439
 
11
440
  // src/InstanceManager.ts
12
- import { exec as exec2, spawn } from "node:child_process";
13
441
  import { EventEmitter } from "node:events";
14
- import { promisify as promisify2 } from "node:util";
15
- import { createLoggers as createLoggers2 } from "@mcesystems/tool-debug-g4";
442
+ import { createLoggers as createLoggers3 } from "@mcesystems/tool-debug-g4";
16
443
 
17
- // src/LockdownSync.ts
444
+ // src/usbipd.ts
18
445
  import { exec } from "node:child_process";
19
- import { existsSync, mkdirSync } from "node:fs";
20
- import { join } from "node:path";
21
446
  import { promisify } from "node:util";
22
447
  import { createLoggers } from "@mcesystems/tool-debug-g4";
23
- var { logInfo, logWarning } = createLoggers("usbmuxd-instance-manager");
448
+ var { logInfo, logWarning } = createLoggers("usbipd");
24
449
  var execAsync = promisify(exec);
25
- var ALPINE_LOCKDOWN_DIR = "/var/lib/lockdown";
26
- var SYSTEM_CONFIG_PLIST = "SystemConfiguration.plist";
27
- function windowsPathToWsl(windowsPath) {
28
- const normalized = windowsPath.replace(/\\/g, "/").trim();
29
- const driveMatch = normalized.match(/^([a-zA-Z]):\/?(.*)$/);
30
- if (driveMatch) {
31
- const drive = driveMatch[1].toLowerCase();
32
- const rest = driveMatch[2] || "";
33
- return `/mnt/${drive}${rest ? `/${rest}` : ""}`;
34
- }
35
- return normalized;
36
- }
37
- async function syncToAlpine(udid, config2) {
38
- if (!config2.lockdownSyncEnabled || !config2.lockdownWindowsPath?.trim()) {
39
- return;
40
- }
41
- const windowsDir = config2.lockdownWindowsPath.trim();
42
- const windowsFile = join(windowsDir, `${udid}.plist`);
43
- if (!existsSync(windowsFile)) {
44
- return;
45
- }
46
- const distro = config2.wslDistribution || "alpine-usbmuxd-build";
47
- const wslSource = windowsPathToWsl(windowsFile);
48
- const wslDestDir = ALPINE_LOCKDOWN_DIR;
49
- try {
50
- await execAsync(
51
- `wsl -d ${distro} -- sh -c "mkdir -p ${wslDestDir} && cp '${wslSource}' '${wslDestDir}/'"`
52
- );
53
- logInfo(`Lockdown sync: copied ${udid}.plist to Alpine`);
54
- } catch (error) {
450
+ var Usbipd = class {
451
+ constructor(usbipdPath) {
452
+ this.usbipdPath = usbipdPath;
453
+ }
454
+ async unbindAllDevicesFromWsl() {
455
+ try {
456
+ await execAsync(`"${this.usbipdPath}" unbind -a`);
457
+ logInfo("All devices unbound from WSL");
458
+ } catch (error) {
459
+ logWarning(`Failed to unbind all devices from WSL: ${error}`);
460
+ }
461
+ }
462
+ async detachAllDevicesFromWsl() {
463
+ try {
464
+ await execAsync(`"${this.usbipdPath}" detach -a`);
465
+ logInfo("All devices detached from WSL");
466
+ } catch (error) {
467
+ logWarning(`Failed to detach all devices from WSL: ${error}`);
468
+ }
469
+ }
470
+ /**
471
+ * Detach a device from WSL via usbipd
472
+ */
473
+ async detachDeviceFromWsl(busId) {
474
+ try {
475
+ await execAsync(`"${this.usbipdPath}" detach --busid=${busId}`);
476
+ logInfo(`Device ${busId} detached from WSL`);
477
+ } catch (error) {
478
+ const message = error instanceof Error ? error.message : String(error);
479
+ if (message.includes("no device with busid") || message.includes("There is no device")) {
480
+ logInfo(`Device ${busId} already detached`);
481
+ } else {
482
+ logWarning(`Failed to detach device ${busId}: ${error}`);
483
+ }
484
+ }
485
+ }
486
+ /**
487
+ * Attach a device to WSL via usbipd
488
+ * Note: This requires administrator privileges
489
+ */
490
+ async attachDeviceToWsl(busId, wsl, distro) {
491
+ try {
492
+ await wsl.ensureWslRunning();
493
+ logInfo(`Binding device ${busId}...`);
494
+ await execAsync(`"${this.usbipdPath}" bind --busid ${busId} --force`);
495
+ if (distro) {
496
+ logInfo(`Attaching device ${busId} to WSL distribution ${distro}...`);
497
+ await execAsync(`"${this.usbipdPath}" attach --wsl=${distro} --busid=${busId}`);
498
+ } else {
499
+ logInfo(`Attaching device ${busId} to default WSL...`);
500
+ await execAsync(`"${this.usbipdPath}" attach --wsl --busid=${busId}`);
501
+ }
502
+ logInfo(`Device ${busId} attached to WSL successfully`);
503
+ return true;
504
+ } catch (error) {
505
+ const message = error instanceof Error ? error.message : String(error);
506
+ if (message.includes("already attached to a client")) {
507
+ logInfo(`Device ${busId} is already attached to WSL, continuing`);
508
+ return true;
509
+ }
510
+ logWarning(`Failed to attach device ${busId} to WSL: ${error}`);
511
+ return false;
512
+ }
513
+ }
514
+ /**
515
+ * Parse usbipd list output to extract device information
516
+ */
517
+ parseUsbipdList(output) {
518
+ const devices = [];
519
+ const lines = output.split(/\r?\n/);
520
+ for (const line of lines) {
521
+ const match = line.match(/^(\d+-\d+)\s+([0-9a-f]{4}):([0-9a-f]{4})\s+(.+)$/i);
522
+ if (match) {
523
+ const rest = match[4].trim();
524
+ const stateMatch = rest.match(/^(.+?)\s{2,}(\S.*)$/);
525
+ const description = stateMatch ? stateMatch[1].trim() : rest;
526
+ const state = stateMatch ? stateMatch[2].trim() : "Unknown";
527
+ devices.push({
528
+ busId: match[1],
529
+ vid: match[2].toUpperCase(),
530
+ pid: match[3].toUpperCase(),
531
+ description,
532
+ state
533
+ });
534
+ }
535
+ }
536
+ return devices;
537
+ }
538
+ /**
539
+ * Find the usbipd bus ID for a device by matching VID/PID
540
+ */
541
+ async findBusIdForDevice(device) {
542
+ try {
543
+ const { stdout } = await execAsync(`"${this.usbipdPath}" list`);
544
+ const usbipdDevices = this.parseUsbipdList(stdout);
545
+ const deviceVid = device.vid.toString(16).toUpperCase().padStart(4, "0");
546
+ const devicePid = device.pid.toString(16).toUpperCase().padStart(4, "0");
547
+ logInfo(`Looking for device with VID:PID ${deviceVid}:${devicePid}`);
548
+ logInfo(`Found ${usbipdDevices.length} devices from usbipd list`);
549
+ const match = usbipdDevices.find((d) => d.vid === deviceVid && d.pid === devicePid);
550
+ if (match) {
551
+ logInfo(
552
+ `Found usbipd bus ID ${match.busId} for device ${device.deviceId} (${deviceVid}:${devicePid})`
553
+ );
554
+ return match.busId;
555
+ }
556
+ throw new Error(
557
+ `Could not find usbipd bus ID for device ${device.deviceId} (${deviceVid}:${devicePid})`
558
+ );
559
+ } catch (error) {
560
+ throw new Error(`Failed to run usbipd list: ${error}`);
561
+ }
562
+ }
563
+ };
564
+
565
+ // src/wsl.ts
566
+ import { exec as exec2, spawn } from "node:child_process";
567
+ import { existsSync, mkdirSync } from "node:fs";
568
+ import { join } from "node:path";
569
+ import { promisify as promisify2 } from "node:util";
570
+ import { createLoggers as createLoggers2 } from "@mcesystems/tool-debug-g4";
571
+ var { logInfo: logInfo2, logWarning: logWarning2, logDetail } = createLoggers2("wsl");
572
+ var execAsync2 = promisify2(exec2);
573
+ var Wsl2 = class {
574
+ constructor(wslDistribution) {
575
+ this.wslDistribution = wslDistribution;
576
+ }
577
+ ALPINE_LOCKDOWN_DIR = "/var/lib/lockdown";
578
+ SYSTEM_CONFIG_PLIST = "SystemConfiguration.plist";
579
+ wslIpAddress = null;
580
+ /**
581
+ * Detect the WSL2 IP address for the configured distribution
582
+ * This IP is needed to connect from Windows to services inside WSL
583
+ */
584
+ async detectWslIpAddress() {
585
+ if (this.wslIpAddress) {
586
+ return this.wslIpAddress;
587
+ }
588
+ const distro = this.wslDistribution || "alpine-usbmuxd-build";
589
+ try {
590
+ const { stdout } = await execAsync2(`wsl -d ${distro} -- ip -4 addr show eth0`);
591
+ const match = stdout.match(/inet\s+(\d+\.\d+\.\d+\.\d+)/);
592
+ if (match) {
593
+ this.wslIpAddress = match[1];
594
+ logInfo2(`Detected WSL IP address: ${this.wslIpAddress}`);
595
+ return this.wslIpAddress;
596
+ }
597
+ } catch (error) {
598
+ logWarning2(`Failed to detect WSL IP via ip addr: ${error}`);
599
+ }
600
+ try {
601
+ const { stdout } = await execAsync2(`wsl -d ${distro} -- hostname -I`);
602
+ const ip = stdout.trim().split(/\s+/)[0];
603
+ if (ip && /^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
604
+ this.wslIpAddress = ip;
605
+ logInfo2(`Detected WSL IP address (hostname): ${this.wslIpAddress}`);
606
+ return this.wslIpAddress;
607
+ }
608
+ } catch (error) {
609
+ logWarning2(`Failed to detect WSL IP via hostname: ${error}`);
610
+ }
611
+ logWarning2("Could not detect WSL IP, falling back to localhost");
612
+ this.wslIpAddress = "127.0.0.1";
613
+ return this.wslIpAddress;
614
+ }
615
+ /**
616
+ * Ensure the WSL distribution is running
617
+ */
618
+ async ensureWslRunning() {
619
+ const distro = this.wslDistribution;
620
+ try {
621
+ if (distro) {
622
+ logInfo2(`Starting WSL distribution: ${distro}...`);
623
+ await execAsync2(`wsl -d ${distro} -- echo "WSL started"`);
624
+ } else {
625
+ logInfo2("Starting default WSL distribution...");
626
+ await execAsync2(`wsl -- echo "WSL started"`);
627
+ }
628
+ return true;
629
+ } catch (error) {
630
+ logWarning2(`Failed to start WSL: ${error}`);
631
+ return false;
632
+ }
633
+ }
634
+ /**
635
+ * Create a new usbmuxd instance
636
+ */
637
+ async createInstance({
638
+ id,
639
+ basePort,
640
+ verboseLogging,
641
+ usbmuxdPath,
642
+ onLog,
643
+ onExit
644
+ }) {
645
+ const port = basePort + id - 1;
646
+ logInfo2(`Creating instance ${id} on port ${port}`);
647
+ const host = await this.detectWslIpAddress();
648
+ const usbmuxdArgs = [
649
+ "-f",
650
+ // Foreground
651
+ "-v",
652
+ // Verbose (if enabled)
653
+ "-S",
654
+ `0.0.0.0:${port}`,
655
+ // Listen on all interfaces (for Windows → WSL2)
656
+ "--pidfile",
657
+ "NONE"
658
+ ];
659
+ if (!verboseLogging) {
660
+ usbmuxdArgs.splice(1, 1);
661
+ }
662
+ const wslArgs = [
663
+ "-d",
664
+ this.wslDistribution || "alpine-usbmuxd-build",
665
+ usbmuxdPath,
666
+ ...usbmuxdArgs
667
+ ];
668
+ const process2 = spawn("wsl", wslArgs, {
669
+ stdio: ["ignore", "pipe", "pipe"],
670
+ detached: true,
671
+ windowsHide: false
672
+ });
673
+ process2.stdout?.on("data", (data) => {
674
+ onLog(data.toString().trim());
675
+ });
676
+ process2.stderr?.on("data", (data) => {
677
+ onLog(data.toString().trim());
678
+ });
679
+ process2.on("exit", onExit);
680
+ const pid = process2.pid;
681
+ if (pid === void 0) {
682
+ process2.kill("SIGKILL");
683
+ throw new Error("Failed to get PID for usbmuxd instance");
684
+ }
685
+ const instance = {
686
+ id,
687
+ host,
688
+ port,
689
+ pid,
690
+ deviceUdids: [],
691
+ startedAt: /* @__PURE__ */ new Date()
692
+ };
693
+ return { instance, process: process2 };
694
+ }
695
+ /**
696
+ * Convert a Windows path to the equivalent path inside WSL (e.g. C:\foo\bar -> /mnt/c/foo/bar).
697
+ */
698
+ windowsPathToWsl(windowsPath) {
699
+ const normalized = windowsPath.replace(/\\/g, "/").trim();
700
+ const driveMatch = normalized.match(/^([a-zA-Z]):\/?(.*)$/);
701
+ if (driveMatch) {
702
+ const drive = driveMatch[1].toLowerCase();
703
+ const rest = driveMatch[2] || "";
704
+ return `/mnt/${drive}${rest ? `/${rest}` : ""}`;
705
+ }
706
+ return normalized;
707
+ }
708
+ /**
709
+ * Get the native Apple lockdown directory path for the current platform.
710
+ * This is where Apple/iTunes stores pairing records natively.
711
+ */
712
+ getAppleLockdownPath() {
713
+ if (process.platform === "win32") {
714
+ return join(process.env.ProgramData ?? "C:\\ProgramData", "Apple", "Lockdown");
715
+ }
716
+ if (process.platform === "darwin") {
717
+ return "/var/db/lockdown";
718
+ }
719
+ if (process.platform === "linux") {
720
+ return "/var/lib/lockdown";
721
+ }
722
+ return null;
723
+ }
724
+ /**
725
+ * Copy a single device's lockdown plist from the native Apple lockdown directory
726
+ * to Alpine so usbmuxd can use it (skip pairing).
727
+ *
728
+ * Uses the platform-specific Apple/iTunes lockdown directory
729
+ * (e.g. C:\ProgramData\Apple\Lockdown on Windows, /var/db/lockdown on macOS).
730
+ *
731
+ * No-op if the lockdown directory cannot be determined or files don't exist.
732
+ */
733
+ async syncToAlpine(udid) {
734
+ const lockdownDir = this.getAppleLockdownPath();
735
+ if (!lockdownDir) {
736
+ logWarning2("Lockdown sync to Alpine: unsupported platform, skipping");
737
+ return;
738
+ }
739
+ logDetail(`Lockdown sync to Alpine: source dir = ${lockdownDir}, udid = ${udid}`);
740
+ const distro = this.wslDistribution || "alpine-usbmuxd-build";
741
+ const wslDestDir = this.ALPINE_LOCKDOWN_DIR;
742
+ const devicePlistFile = `${udid}.plist`;
743
+ const devicePlistPath = join(lockdownDir, devicePlistFile);
744
+ if (existsSync(devicePlistPath)) {
745
+ await this.copyFileToAlpine(
746
+ this.windowsPathToWsl(devicePlistPath),
747
+ wslDestDir,
748
+ distro,
749
+ devicePlistFile
750
+ );
751
+ } else {
752
+ logDetail(
753
+ `Lockdown sync to Alpine: ${devicePlistFile} not found at ${devicePlistPath}, skipping`
754
+ );
755
+ }
756
+ const systemConfigPath = join(lockdownDir, this.SYSTEM_CONFIG_PLIST);
757
+ if (existsSync(systemConfigPath)) {
758
+ await this.copyFileToAlpine(
759
+ this.windowsPathToWsl(systemConfigPath),
760
+ wslDestDir,
761
+ distro,
762
+ this.SYSTEM_CONFIG_PLIST
763
+ );
764
+ } else {
765
+ logDetail(
766
+ `Lockdown sync to Alpine: ${this.SYSTEM_CONFIG_PLIST} not found at ${systemConfigPath}, skipping`
767
+ );
768
+ }
769
+ }
770
+ /**
771
+ * Copy a single file into the Alpine lockdown directory.
772
+ * Falls back to sudo if the initial copy fails.
773
+ */
774
+ async copyFileToAlpine(wslSource, wslDestDir, distro, fileName) {
775
+ try {
776
+ await execAsync2(
777
+ `wsl -d ${distro} -- sh -c "mkdir -p ${wslDestDir} && cp '${wslSource}' '${wslDestDir}/'"`
778
+ );
779
+ logInfo2(`Lockdown sync: copied ${fileName} to Alpine`);
780
+ } catch (error) {
781
+ try {
782
+ await execAsync2(
783
+ `wsl -d ${distro} -- sh -c "mkdir -p ${wslDestDir} && sudo cp '${wslSource}' '${wslDestDir}/'"`
784
+ );
785
+ logInfo2(`Lockdown sync: copied ${fileName} to Alpine (via sudo)`);
786
+ } catch (sudoError) {
787
+ logWarning2(
788
+ `Lockdown sync to Alpine failed for ${fileName}: ${error}. Sudo fallback failed: ${sudoError}. Ensure /var/lib/lockdown is writable or use passwordless sudo.`
789
+ );
790
+ }
791
+ }
792
+ }
793
+ /**
794
+ * Copy lockdown plists from Alpine back to the native Apple lockdown directory
795
+ * (for devices that were assigned this session).
796
+ * Also copies SystemConfiguration.plist if present in Alpine.
797
+ *
798
+ * No-op if the lockdown directory cannot be determined.
799
+ */
800
+ async syncFromAlpine(udids) {
801
+ logInfo2(`Lockdown sync from Alpine: starting (${udids.length} device(s))`);
802
+ const lockdownDir = this.getAppleLockdownPath();
803
+ if (!lockdownDir) {
804
+ logWarning2("Lockdown sync: unsupported platform, skipping");
805
+ return;
806
+ }
807
+ logDetail(`Lockdown sync: target dir = ${lockdownDir}`);
808
+ try {
809
+ mkdirSync(lockdownDir, { recursive: true });
810
+ } catch (error) {
811
+ logWarning2(`Lockdown sync: could not create lockdown dir ${lockdownDir}: ${error}`);
812
+ return;
813
+ }
814
+ const distro = this.wslDistribution || "alpine-usbmuxd-build";
815
+ const wslDestDir = this.windowsPathToWsl(lockdownDir);
55
816
  try {
56
- await execAsync(
57
- `wsl -d ${distro} -- sh -c "mkdir -p ${wslDestDir} && sudo cp '${wslSource}' '${wslDestDir}/'"`
58
- );
59
- logInfo(`Lockdown sync: copied ${udid}.plist to Alpine (via sudo)`);
60
- } catch (sudoError) {
61
- logWarning(
62
- `Lockdown sync to Alpine failed for ${udid}: ${error}. Sudo fallback failed: ${sudoError}. Ensure /var/lib/lockdown is writable or use passwordless sudo.`
63
- );
817
+ const { stdout } = await execAsync2(`wsl -d ${distro} -- ls -la ${this.ALPINE_LOCKDOWN_DIR}/`);
818
+ logDetail(`Lockdown sync: Alpine ${this.ALPINE_LOCKDOWN_DIR} contents:
819
+ ${stdout}`);
820
+ } catch (error) {
821
+ logWarning2(`Lockdown sync: could not list Alpine lockdown dir: ${error}`);
64
822
  }
823
+ const filesToSync = [...udids.map((udid) => `${udid}.plist`), this.SYSTEM_CONFIG_PLIST];
824
+ logDetail(`Lockdown sync: files to sync = [${filesToSync.join(", ")}]`);
825
+ for (const fileName of filesToSync) {
826
+ await this.copyFileFromAlpine(fileName, wslDestDir, distro, lockdownDir);
827
+ }
828
+ logInfo2("Lockdown sync from Alpine: done");
65
829
  }
66
- }
67
- async function syncFromAlpine(udids, config2) {
68
- if (!config2.lockdownSyncEnabled || !config2.lockdownWindowsPath?.trim()) {
69
- return;
70
- }
71
- const windowsDir = config2.lockdownWindowsPath.trim();
72
- try {
73
- mkdirSync(windowsDir, { recursive: true });
74
- } catch (error) {
75
- logWarning(`Lockdown sync: could not create Windows dir ${windowsDir}: ${error}`);
76
- return;
77
- }
78
- const distro = config2.wslDistribution || "alpine-usbmuxd-build";
79
- const wslDestDir = windowsPathToWsl(windowsDir);
80
- for (const udid of udids) {
81
- const plist = `${udid}.plist`;
830
+ /**
831
+ * Copy a single file from the Alpine lockdown directory back to Windows.
832
+ * Skips if the file does not exist in Alpine; logs a warning if the copy fails.
833
+ */
834
+ async copyFileFromAlpine(fileName, wslDestDir, distro, lockdownDir) {
835
+ const alpinePath = `${this.ALPINE_LOCKDOWN_DIR}/${fileName}`;
82
836
  try {
83
- await execAsync(
84
- `wsl -d ${distro} -- sh -c "test -f '${ALPINE_LOCKDOWN_DIR}/${plist}' && cp '${ALPINE_LOCKDOWN_DIR}/${plist}' '${wslDestDir}/'"`
85
- );
86
- logInfo(`Lockdown sync: copied ${plist} from Alpine to Windows`);
837
+ await execAsync2(`wsl -d ${distro} -- test -f '${alpinePath}'`);
87
838
  } catch {
839
+ logDetail(`Lockdown sync: ${fileName} not found in Alpine (${alpinePath}), skipping`);
840
+ return;
841
+ }
842
+ logDetail(`Lockdown sync: copying ${fileName} \u2192 ${wslDestDir}/`);
843
+ try {
844
+ await execAsync2(`wsl -d ${distro} -- cp -f '${alpinePath}' '${wslDestDir}/'`);
845
+ logInfo2(`Lockdown sync: copied ${fileName} from Alpine to ${lockdownDir}`);
846
+ } catch (error) {
847
+ logWarning2(`Lockdown sync: failed to copy ${fileName} from Alpine to ${lockdownDir}: ${error}`);
88
848
  }
89
849
  }
90
- try {
91
- await execAsync(
92
- `wsl -d ${distro} -- sh -c "test -f '${ALPINE_LOCKDOWN_DIR}/${SYSTEM_CONFIG_PLIST}' && cp '${ALPINE_LOCKDOWN_DIR}/${SYSTEM_CONFIG_PLIST}' '${wslDestDir}/'"`
93
- );
94
- logInfo(`Lockdown sync: copied ${SYSTEM_CONFIG_PLIST} from Alpine to Windows`);
95
- } catch {
96
- }
97
- }
850
+ };
98
851
 
99
852
  // src/InstanceManager.ts
100
- var { logInfo: logInfo2, logWarning: logWarning2 } = createLoggers2("usbmuxd-instance-manager");
101
- var execAsync2 = promisify2(exec2);
102
- var USBIPD_PATH = '"C:\\Program Files\\usbipd-win\\usbipd.exe"';
103
- function parseUsbipdList(output) {
104
- const devices = [];
105
- const lines = output.split(/\r?\n/);
106
- for (const line of lines) {
107
- const match = line.match(/^(\d+-\d+)\s+([0-9a-f]{4}):([0-9a-f]{4})\s+(.+)$/i);
108
- if (match) {
109
- const rest = match[4].trim();
110
- const stateMatch = rest.match(/^(.+?)\s{2,}(\S.*)$/);
111
- const description = stateMatch ? stateMatch[1].trim() : rest;
112
- const state = stateMatch ? stateMatch[2].trim() : "Unknown";
113
- devices.push({
114
- busId: match[1],
115
- vid: match[2].toUpperCase(),
116
- pid: match[3].toUpperCase(),
117
- description,
118
- state
119
- });
120
- }
121
- }
122
- return devices;
123
- }
853
+ var { logWarning: logWarning3, logDetail: logDetail2, logDataObject, logTask } = createLoggers3("instance-manager");
124
854
  var DEFAULT_CONFIG = {
125
855
  batchSize: 4,
126
856
  basePort: 27015,
127
- maxInstances: 20,
857
+ maxInstances: 5,
128
858
  usbmuxdPath: "usbmuxd",
129
859
  // Path inside WSL2
130
860
  wslDistribution: "alpine-usbmuxd-build",
131
861
  // Alpine WSL2 distribution name
132
862
  verboseLogging: true,
133
- appleVendorId: "05AC",
134
- lockdownWindowsPath: "C:\\ProgramData\\mce\\lockdown",
135
- lockdownSyncEnabled: true
863
+ appleVendorId: "05AC"
136
864
  };
137
865
  var InstanceManager = class extends EventEmitter {
138
- config;
866
+ /** Map of usbmuxd instance IDs to instances */
139
867
  instances = /* @__PURE__ */ new Map();
868
+ /** Map of device IDs to device mappings host:port */
140
869
  deviceMappings = /* @__PURE__ */ new Map();
870
+ /** Map of usbmuxd instance IDs to child processes running it on wsl2*/
141
871
  processes = /* @__PURE__ */ new Map();
142
- nextInstanceId = 1;
143
- startedAt = null;
144
- isRunning = false;
145
872
  /** Tracks which devices have been attached to WSL */
146
873
  attachedDevices = /* @__PURE__ */ new Set();
147
- /** Device IDs currently in the attach flow (ignore disconnect until attach completes) */
874
+ /** Device IDs currently in the attach flow
875
+ * (ignore disconnect until attach completes) */
148
876
  pendingAttachDevices = /* @__PURE__ */ new Set();
149
- /** Cached WSL IP address for connecting from Windows */
150
- wslIpAddress = null;
877
+ config;
878
+ nextInstanceId = 1;
879
+ startedAt = null;
880
+ isRunning = false;
881
+ wsl;
882
+ usbipd;
151
883
  constructor(config2 = {}) {
152
884
  super();
153
885
  this.config = { ...DEFAULT_CONFIG, ...config2 };
154
- }
155
- /**
156
- * Detect the WSL2 IP address for the configured distribution
157
- * This IP is needed to connect from Windows to services inside WSL
158
- */
159
- async detectWslIpAddress() {
160
- if (this.wslIpAddress) {
161
- return this.wslIpAddress;
162
- }
163
- const distro = this.config.wslDistribution || "alpine-usbmuxd-build";
164
- try {
165
- const { stdout } = await execAsync2(`wsl -d ${distro} -- ip -4 addr show eth0`);
166
- const match = stdout.match(/inet\s+(\d+\.\d+\.\d+\.\d+)/);
167
- if (match) {
168
- this.wslIpAddress = match[1];
169
- logInfo2(`Detected WSL IP address: ${this.wslIpAddress}`);
170
- return this.wslIpAddress;
171
- }
172
- } catch (error) {
173
- logWarning2(`Failed to detect WSL IP via ip addr: ${error}`);
174
- }
175
- try {
176
- const { stdout } = await execAsync2(`wsl -d ${distro} -- hostname -I`);
177
- const ip = stdout.trim().split(/\s+/)[0];
178
- if (ip && /^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
179
- this.wslIpAddress = ip;
180
- logInfo2(`Detected WSL IP address (hostname): ${this.wslIpAddress}`);
181
- return this.wslIpAddress;
182
- }
183
- } catch (error) {
184
- logWarning2(`Failed to detect WSL IP via hostname: ${error}`);
185
- }
186
- logWarning2("Could not detect WSL IP, falling back to localhost");
187
- this.wslIpAddress = "127.0.0.1";
188
- return this.wslIpAddress;
886
+ this.wsl = new Wsl2(this.config.wslDistribution);
887
+ this.usbipd = new Usbipd(process.env.USBIPD_PATH ?? "usbipd");
189
888
  }
190
889
  /**
191
890
  * Start the instance manager
@@ -196,7 +895,6 @@ var InstanceManager = class extends EventEmitter {
196
895
  }
197
896
  this.isRunning = true;
198
897
  this.startedAt = /* @__PURE__ */ new Date();
199
- this.emit("started");
200
898
  }
201
899
  /**
202
900
  * Stop the instance manager and all instances
@@ -207,147 +905,122 @@ var InstanceManager = class extends EventEmitter {
207
905
  }
208
906
  this.isRunning = false;
209
907
  const udids = Array.from(this.deviceMappings.keys());
210
- await syncFromAlpine(udids, this.config);
211
- const detachPromises = Array.from(this.attachedDevices).map(
212
- (busId) => this.detachDeviceFromWsl(busId)
213
- );
214
- await Promise.all(detachPromises);
908
+ await this.wsl.syncFromAlpine(udids);
909
+ await this.usbipd.detachAllDevicesFromWsl();
910
+ await this.usbipd.unbindAllDevicesFromWsl();
215
911
  this.attachedDevices.clear();
216
912
  const stopPromises = Array.from(this.instances.keys()).map((id) => this.stopInstance(id));
217
913
  await Promise.all(stopPromises);
218
914
  this.instances.clear();
219
915
  this.deviceMappings.clear();
220
916
  this.processes.clear();
221
- this.emit("stopped");
222
917
  }
223
- /**
224
- * Find the usbipd bus ID for a device by matching VID/PID
225
- */
226
- async findBusIdForDevice(device) {
227
- try {
228
- const { stdout } = await execAsync2(`${USBIPD_PATH} list`);
229
- const usbipdDevices = parseUsbipdList(stdout);
230
- const deviceVid = device.vid.toString(16).toUpperCase().padStart(4, "0");
231
- const devicePid = device.pid.toString(16).toUpperCase().padStart(4, "0");
232
- logInfo2(`Looking for device with VID:PID ${deviceVid}:${devicePid}`);
233
- logInfo2(`Found ${usbipdDevices.length} devices from usbipd list`);
234
- const match = usbipdDevices.find((d) => d.vid === deviceVid && d.pid === devicePid);
235
- if (match) {
236
- logInfo2(
237
- `Found usbipd bus ID ${match.busId} for device ${device.deviceId} (${deviceVid}:${devicePid})`
238
- );
239
- return match.busId;
918
+ async attachToWsl({
919
+ busId,
920
+ deviceId
921
+ }) {
922
+ if (!this.attachedDevices.has(busId)) {
923
+ this.pendingAttachDevices.add(deviceId);
924
+ if (!this.config.wslDistribution) {
925
+ throw new Error("WSL distribution not configured");
240
926
  }
241
- for (const d of usbipdDevices) {
242
- logInfo2(` usbipd device: ${d.busId} ${d.vid}:${d.pid} "${d.description}"`);
243
- }
244
- logWarning2(
245
- `Could not find usbipd bus ID for device ${device.deviceId} (${deviceVid}:${devicePid})`
927
+ const attached = await this.usbipd.attachDeviceToWsl(
928
+ busId,
929
+ this.wsl,
930
+ this.config.wslDistribution
246
931
  );
247
- return null;
248
- } catch (error) {
249
- logWarning2(`Failed to run usbipd list: ${error}`);
250
- return null;
932
+ if (attached) {
933
+ this.attachedDevices.add(busId);
934
+ }
251
935
  }
252
936
  }
253
- /**
254
- * Ensure the WSL distribution is running
255
- */
256
- async ensureWslRunning() {
257
- const distro = this.config.wslDistribution;
258
- try {
259
- if (distro) {
260
- logInfo2(`Starting WSL distribution: ${distro}...`);
261
- await execAsync2(`wsl -d ${distro} -- echo "WSL started"`);
262
- } else {
263
- logInfo2("Starting default WSL distribution...");
264
- await execAsync2(`wsl -- echo "WSL started"`);
265
- }
266
- return true;
267
- } catch (error) {
268
- logWarning2(`Failed to start WSL: ${error}`);
269
- return false;
937
+ async removeDevice(mapping) {
938
+ const instance = await this.detachFromWsl({ deviceId: mapping.udid });
939
+ if (!instance) {
940
+ throw new Error(`Instance ${mapping.instanceId} not found`);
941
+ }
942
+ if (mapping.busId) {
943
+ await this.usbipd.detachDeviceFromWsl(mapping.busId);
944
+ this.attachedDevices.delete(mapping.busId);
270
945
  }
946
+ const deviceIndex = instance.deviceUdids.indexOf(mapping.udid);
947
+ if (deviceIndex > -1) {
948
+ instance.deviceUdids.splice(deviceIndex, 1);
949
+ }
950
+ this.deviceMappings.delete(mapping.udid);
951
+ return instance;
271
952
  }
272
- /**
273
- * Attach a device to WSL via usbipd
274
- * Note: This requires administrator privileges
275
- */
276
- async attachDeviceToWsl(busId) {
277
- const distro = this.config.wslDistribution;
278
- try {
279
- await this.ensureWslRunning();
280
- logInfo2(`Binding device ${busId}...`);
281
- await execAsync2(`${USBIPD_PATH} bind --busid ${busId} --force`);
282
- if (distro) {
283
- logInfo2(`Attaching device ${busId} to WSL distribution ${distro}...`);
284
- await execAsync2(`${USBIPD_PATH} attach --wsl=${distro} --busid=${busId}`);
285
- } else {
286
- logInfo2(`Attaching device ${busId} to default WSL...`);
287
- await execAsync2(`${USBIPD_PATH} attach --wsl --busid=${busId}`);
288
- }
289
- logInfo2(`Device ${busId} attached to WSL successfully`);
290
- return true;
291
- } catch (error) {
292
- const message = error instanceof Error ? error.message : String(error);
293
- if (message.includes("already attached to a client")) {
294
- logInfo2(`Device ${busId} is already attached to WSL, continuing`);
295
- return true;
953
+ async onUsbmuxdInstanceEnd({
954
+ instanceId,
955
+ code,
956
+ signal
957
+ }) {
958
+ this.emit("instance-exited", {
959
+ instanceId,
960
+ code,
961
+ signal
962
+ });
963
+ for (const [_, mapping] of this.deviceMappings.entries()) {
964
+ if (mapping.instanceId === instanceId) {
965
+ this.removeDevice(mapping);
296
966
  }
297
- logWarning2(`Failed to attach device ${busId} to WSL: ${error}`);
298
- return false;
299
967
  }
968
+ this.instances.delete(instanceId);
969
+ this.processes.get(instanceId)?.kill();
970
+ this.processes.delete(instanceId);
300
971
  }
301
- /**
302
- * Detach a device from WSL via usbipd
303
- */
304
- async detachDeviceFromWsl(busId) {
305
- try {
306
- await execAsync2(`${USBIPD_PATH} detach --busid=${busId}`);
307
- logInfo2(`Device ${busId} detached from WSL`);
308
- } catch (error) {
309
- const message = error instanceof Error ? error.message : String(error);
310
- if (message.includes("no device with busid") || message.includes("There is no device")) {
311
- logInfo2(`Device ${busId} already detached`);
312
- } else {
313
- logWarning2(`Failed to detach device ${busId}: ${error}`);
972
+ async onUsbmuxdInstanceStart({
973
+ instance,
974
+ process: process2
975
+ }) {
976
+ this.instances.set(instance.id, instance);
977
+ this.processes.set(instance.id, process2);
978
+ this.emit("instance-started", {
979
+ instanceId: instance.id
980
+ });
981
+ }
982
+ async getInstance() {
983
+ let targetInstance = this.findInstanceWithCapacity();
984
+ if (!targetInstance) {
985
+ if (this.instances.size >= this.config.maxInstances) {
986
+ throw new Error(`Maximum number of instances (${this.config.maxInstances}) reached`);
314
987
  }
988
+ const newInstanceId = this.nextInstanceId++;
989
+ let onExit = (code, signal) => {
990
+ this.onUsbmuxdInstanceEnd({ instanceId: newInstanceId, code, signal });
991
+ };
992
+ onExit = onExit.bind(this);
993
+ const { instance, process: process2 } = await this.wsl.createInstance({
994
+ id: newInstanceId,
995
+ basePort: this.config.basePort,
996
+ verboseLogging: this.config.verboseLogging,
997
+ usbmuxdPath: this.config.usbmuxdPath,
998
+ onLog: (message) => this.emit("instance-log", {
999
+ instanceId: newInstanceId,
1000
+ level: "info",
1001
+ message
1002
+ }),
1003
+ onExit
1004
+ });
1005
+ this.onUsbmuxdInstanceStart({ instance, process: process2 });
1006
+ targetInstance = instance;
315
1007
  }
1008
+ return targetInstance;
316
1009
  }
317
1010
  /**
318
- * Handle device connection
1011
+ * Handle device connection only for not attached devices
319
1012
  * Attaches device to WSL, then assigns to an existing instance or creates a new one
320
1013
  */
321
1014
  async onDeviceConnected(device) {
322
- if (this.deviceMappings.has(device.deviceId)) {
323
- logWarning2(`Device ${device.deviceId} is already connected`);
1015
+ const busId = await this.usbipd.findBusIdForDevice(device);
1016
+ if (this.attachedDevices.has(busId)) {
1017
+ logTask(`Device ${device.deviceId} is already attached. skipping onDeviceConnected...`);
324
1018
  return;
325
1019
  }
326
- await syncToAlpine(device.deviceId, this.config);
327
- const busId = await this.findBusIdForDevice(device);
328
- if (!busId) {
329
- logWarning2(`Cannot attach device ${device.deviceId} - bus ID not found`);
330
- }
331
- if (busId && !this.attachedDevices.has(busId)) {
332
- this.pendingAttachDevices.add(device.deviceId);
333
- try {
334
- const attached = await this.attachDeviceToWsl(busId);
335
- if (attached) {
336
- this.attachedDevices.add(busId);
337
- await new Promise((resolve) => setTimeout(resolve, 1e3));
338
- }
339
- } finally {
340
- this.pendingAttachDevices.delete(device.deviceId);
341
- }
342
- }
343
- let targetInstance = this.findInstanceWithCapacity();
344
- if (!targetInstance) {
345
- if (this.instances.size >= this.config.maxInstances) {
346
- throw new Error(`Maximum number of instances (${this.config.maxInstances}) reached`);
347
- }
348
- targetInstance = await this.createInstance();
349
- }
350
- targetInstance.deviceUdids.push(device.deviceId);
1020
+ logDataObject("Device connected", { device });
1021
+ await this.wsl.syncToAlpine(device.deviceId);
1022
+ await this.attachToWsl({ busId, deviceId: device.deviceId });
1023
+ const targetInstance = await this.getInstance();
351
1024
  const mapping = {
352
1025
  udid: device.deviceId,
353
1026
  instanceId: targetInstance.id,
@@ -357,77 +1030,52 @@ var InstanceManager = class extends EventEmitter {
357
1030
  busId: busId ?? void 0
358
1031
  };
359
1032
  this.deviceMappings.set(device.deviceId, mapping);
1033
+ targetInstance.deviceUdids.push(device.deviceId);
360
1034
  this.emit("device-assigned", {
361
1035
  device,
362
1036
  instance: targetInstance,
363
1037
  mapping
364
1038
  });
1039
+ this.pendingAttachDevices.delete(device.deviceId);
365
1040
  }
366
- /**
367
- * Pair a device with the usbmuxd host.
368
- * This is required once per device before most commands will work.
369
- * The pairing record is stored in WSL and persists across restarts.
370
- *
371
- * @param udid Device UDID to pair
372
- * @param goIosPath Optional path to go-ios binary (defaults to "ios")
373
- * @returns true if pairing succeeded, false otherwise
374
- */
375
- async pairDevice(udid, goIosPath = "ios") {
376
- const mapping = this.deviceMappings.get(udid);
1041
+ async detachFromWsl({ deviceId }) {
1042
+ const mapping = this.deviceMappings.get(deviceId);
377
1043
  if (!mapping) {
378
- logWarning2(`Cannot pair device ${udid} - not found in mappings`);
379
- return false;
1044
+ throw new Error(`Device ${deviceId} was not tracked`);
380
1045
  }
381
- try {
382
- const socketAddress = `${mapping.host}:${mapping.port}`;
383
- logInfo2(`Pairing device ${udid} via ${socketAddress}...`);
384
- const { stderr } = await execAsync2(`"${goIosPath}" pair --udid=${udid}`, {
385
- env: { ...process.env, USBMUXD_SOCKET_ADDRESS: socketAddress }
386
- });
387
- if (stderr?.includes("error")) {
388
- logWarning2(`Pairing warning for ${udid}: ${stderr}`);
389
- }
390
- logInfo2(`Device ${udid} paired successfully`);
391
- this.emit("device-paired", { udid, mapping });
392
- return true;
393
- } catch (error) {
394
- logWarning2(`Failed to pair device ${udid}: ${error}`);
395
- return false;
1046
+ if (mapping.busId) {
1047
+ await this.usbipd.detachDeviceFromWsl(mapping.busId);
1048
+ this.attachedDevices.delete(mapping.busId);
396
1049
  }
1050
+ const instance = this.instances.get(mapping.instanceId);
1051
+ if (!instance) {
1052
+ logWarning3(`Instance ${mapping.instanceId} not found`);
1053
+ this.deviceMappings.delete(deviceId);
1054
+ throw new Error(`Instance ${mapping.instanceId} not found`);
1055
+ }
1056
+ return instance;
397
1057
  }
398
1058
  /**
399
- * Handle device disconnection
1059
+ * Handle device disconnection only for attached devices
400
1060
  * Detaches from WSL, removes device from instance, and stops instance if empty
401
1061
  */
402
1062
  async onDeviceDisconnected(device) {
403
- if (this.pendingAttachDevices.has(device.deviceId)) {
1063
+ if (!this.attachedDevices.has(device.deviceId)) {
1064
+ logTask(`Device ${device.deviceId} is not attached. skipping onDeviceDisconnected...`);
404
1065
  return;
405
1066
  }
1067
+ logDataObject("Device disconnected", { device });
406
1068
  const mapping = this.deviceMappings.get(device.deviceId);
407
1069
  if (!mapping) {
408
- logWarning2(`Device ${device.deviceId} was not tracked`);
409
- return;
410
- }
411
- if (mapping.busId) {
412
- await this.detachDeviceFromWsl(mapping.busId);
413
- this.attachedDevices.delete(mapping.busId);
414
- }
415
- const instance = this.instances.get(mapping.instanceId);
416
- if (!instance) {
417
- logWarning2(`Instance ${mapping.instanceId} not found`);
418
- this.deviceMappings.delete(device.deviceId);
419
- return;
1070
+ throw new Error(`Device ${device.deviceId} was not tracked`);
420
1071
  }
421
- const deviceIndex = instance.deviceUdids.indexOf(device.deviceId);
422
- if (deviceIndex > -1) {
423
- instance.deviceUdids.splice(deviceIndex, 1);
424
- }
425
- this.deviceMappings.delete(device.deviceId);
1072
+ const instance = await this.removeDevice(mapping);
426
1073
  this.emit("device-removed", {
427
1074
  device,
428
1075
  instance
429
1076
  });
430
1077
  if (instance.deviceUdids.length === 0) {
1078
+ logDetail2(`Instance ${instance.id} is empty, stopping...`);
431
1079
  await this.stopInstance(instance.id);
432
1080
  }
433
1081
  }
@@ -442,104 +1090,18 @@ var InstanceManager = class extends EventEmitter {
442
1090
  }
443
1091
  return null;
444
1092
  }
445
- /**
446
- * Create a new usbmuxd instance
447
- */
448
- async createInstance() {
449
- const instanceId = this.nextInstanceId++;
450
- const port = this.config.basePort + instanceId - 1;
451
- const host = await this.detectWslIpAddress();
452
- const usbmuxdArgs = [
453
- "-f",
454
- // Foreground
455
- "-v",
456
- // Verbose (if enabled)
457
- "-S",
458
- `0.0.0.0:${port}`,
459
- // Listen on all interfaces (for Windows → WSL2)
460
- "--pidfile",
461
- "NONE"
462
- ];
463
- if (!this.config.verboseLogging) {
464
- usbmuxdArgs.splice(1, 1);
465
- }
466
- const wslArgs = [
467
- "-d",
468
- this.config.wslDistribution || "alpine-usbmuxd-build",
469
- this.config.usbmuxdPath,
470
- ...usbmuxdArgs
471
- ];
472
- const process2 = spawn("wsl", wslArgs, {
473
- stdio: ["ignore", "pipe", "pipe"],
474
- windowsHide: false
475
- // Show console for debugging
476
- });
477
- process2.stdout?.on("data", (data) => {
478
- this.emit("instance-log", {
479
- instanceId,
480
- level: "info",
481
- message: data.toString().trim()
482
- });
483
- });
484
- process2.stderr?.on("data", (data) => {
485
- this.emit("instance-log", {
486
- instanceId,
487
- level: "error",
488
- message: data.toString().trim()
489
- });
490
- });
491
- process2.on("exit", (code, signal) => {
492
- this.emit("instance-exited", {
493
- instanceId,
494
- code,
495
- signal
496
- });
497
- if (this.instances.has(instanceId)) {
498
- this.instances.delete(instanceId);
499
- this.processes.delete(instanceId);
500
- }
501
- });
502
- const pid = process2.pid;
503
- if (pid === void 0) {
504
- process2.kill("SIGKILL");
505
- throw new Error("Failed to get PID for usbmuxd instance");
506
- }
507
- const instance = {
508
- id: instanceId,
509
- host,
510
- port,
511
- pid,
512
- deviceUdids: [],
513
- startedAt: /* @__PURE__ */ new Date()
514
- };
515
- this.instances.set(instanceId, instance);
516
- this.processes.set(instanceId, process2);
517
- this.emit("instance-started", instance);
518
- await new Promise((resolve) => setTimeout(resolve, 500));
519
- return instance;
520
- }
521
1093
  /**
522
1094
  * Stop a specific instance
523
1095
  */
524
1096
  async stopInstance(instanceId) {
525
1097
  const instance = this.instances.get(instanceId);
526
- const process2 = this.processes.get(instanceId);
527
- if (!instance || !process2) {
1098
+ const childProcess = this.processes.get(instanceId);
1099
+ if (!instance || !childProcess) {
528
1100
  return;
529
1101
  }
530
- process2.kill("SIGTERM");
531
- await new Promise((resolve) => {
532
- const timeout = setTimeout(() => {
533
- if (!process2.killed) {
534
- process2.kill("SIGKILL");
535
- }
536
- resolve();
537
- }, 5e3);
538
- process2.once("exit", () => {
539
- clearTimeout(timeout);
540
- resolve();
541
- });
542
- });
1102
+ for (const process2 of this.processes.values()) {
1103
+ process2.kill("SIGKILL");
1104
+ }
543
1105
  this.instances.delete(instanceId);
544
1106
  this.processes.delete(instanceId);
545
1107
  for (const [udid, mapping] of this.deviceMappings.entries()) {
@@ -591,7 +1153,7 @@ var InstanceManager = class extends EventEmitter {
591
1153
  };
592
1154
 
593
1155
  // src/UsbmuxdService.ts
594
- var { logInfo: logInfo3, logError } = createLoggers3("usbmuxd-instance-manager");
1156
+ var { logInfo: logInfo3, logError, logDataObject: logDataObject2 } = createLoggers4("usbmuxd-service");
595
1157
  var UsbmuxdService = class extends EventEmitter2 {
596
1158
  manager;
597
1159
  usbListener;
@@ -615,6 +1177,9 @@ var UsbmuxdService = class extends EventEmitter2 {
615
1177
  this.manager.on(
616
1178
  "device-assigned",
617
1179
  (payload) => {
1180
+ logInfo3(
1181
+ `Device ${payload.device.deviceId} connected to usbmuxd instance at ${payload.mapping.host}:${payload.mapping.port}`
1182
+ );
618
1183
  this.emit("device-assigned", payload);
619
1184
  }
620
1185
  );
@@ -650,12 +1215,7 @@ var UsbmuxdService = class extends EventEmitter2 {
650
1215
  }
651
1216
  const config2 = this.manager.getConfig();
652
1217
  const appleVid = Number.parseInt(config2.appleVendorId, 16);
653
- logInfo3("Starting service...");
654
- logInfo3(`Batch size: ${config2.batchSize} devices per instance`);
655
- logInfo3(`Base port: ${config2.basePort}`);
656
- logInfo3(`Max instances: ${config2.maxInstances}`);
657
- logInfo3(`usbmuxd path: ${config2.usbmuxdPath}`);
658
- logInfo3(`Monitoring Apple devices (VID: ${config2.appleVendorId})`);
1218
+ logDataObject2("Starting service...", { config: config2 });
659
1219
  this.usbListener.onDeviceAdd(async (device) => {
660
1220
  if (device.vid !== appleVid) {
661
1221
  return;
@@ -736,22 +1296,11 @@ var UsbmuxdService = class extends EventEmitter2 {
736
1296
  getConfig() {
737
1297
  return this.manager.getConfig();
738
1298
  }
739
- /**
740
- * Pair a device with the usbmuxd host.
741
- * This is required once per device before most commands will work.
742
- * The pairing record is stored in WSL and persists across restarts.
743
- *
744
- * @param udid Device UDID to pair
745
- * @param goIosPath Optional path to go-ios binary
746
- * @returns true if pairing succeeded, false otherwise
747
- */
748
- async pairDevice(udid, goIosPath) {
749
- return this.manager.pairDevice(udid, goIosPath);
750
- }
751
1299
  };
752
1300
 
753
1301
  // src/cli.ts
754
- var { logInfo: logInfo4, logError: logError2, logHeader, logDataObject } = createLoggers4("usbmuxd-cli");
1302
+ import_dotenv.default.config();
1303
+ var { logInfo: logInfo4, logError: logError2, logHeader, logDataObject: logDataObject3 } = createLoggers5("usbmuxd-cli");
755
1304
  var args = process.argv.slice(2);
756
1305
  var options = {};
757
1306
  for (let i = 0; i < args.length; i++) {
@@ -773,31 +1322,25 @@ var config = {
773
1322
  appleVendorId: options.appleVid || "05AC"
774
1323
  };
775
1324
  logHeader("usbmuxd Instance Manager");
776
- logDataObject("Multi-instance manager for iOS device connections", { config });
1325
+ logDataObject3("Multi-instance manager for iOS device connections", { config });
777
1326
  var service = new UsbmuxdService(config);
778
- process.on("SIGINT", async () => {
1327
+ async function shutdown(eventName) {
779
1328
  logInfo4("");
780
- logInfo4("Received SIGINT, shutting down gracefully...");
1329
+ logInfo4(`Received ${eventName}, shutting down gracefully...`);
781
1330
  try {
782
1331
  await service.stop();
783
1332
  logInfo4("Shutdown complete");
784
1333
  process.exit(0);
785
1334
  } catch (error) {
786
- logError2("Error during shutdown:", error);
1335
+ logError2(`Error during ${eventName}:`, error);
787
1336
  process.exit(1);
788
1337
  }
1338
+ }
1339
+ process.on("SIGINT", async () => {
1340
+ void shutdown("SIGINT");
789
1341
  });
790
1342
  process.on("SIGTERM", async () => {
791
- logInfo4("");
792
- logInfo4("Received SIGTERM, shutting down gracefully...");
793
- try {
794
- await service.stop();
795
- logInfo4("Shutdown complete");
796
- process.exit(0);
797
- } catch (error) {
798
- logError2("Error during shutdown:", error);
799
- process.exit(1);
800
- }
1343
+ void shutdown("SIGTERM");
801
1344
  });
802
1345
  setInterval(() => {
803
1346
  const stats = service.getStats();
@@ -806,7 +1349,7 @@ setInterval(() => {
806
1349
  `Uptime: ${stats.uptimeSeconds}s | Instances: ${stats.instanceCount} | Devices: ${stats.deviceCount}`
807
1350
  );
808
1351
  if (instances.length > 0) {
809
- logDataObject("Active instances:", { instances });
1352
+ logDataObject3("Active instances:", { instances });
810
1353
  }
811
1354
  }, 3e4);
812
1355
  if (args.includes("--help") || args.includes("-h")) {
@@ -830,7 +1373,7 @@ if (args.includes("--help") || args.includes("-h")) {
830
1373
  try {
831
1374
  service.start();
832
1375
  } catch (error) {
833
- logError2("Failed to start service:", error);
834
- process.exit(1);
1376
+ logError2("Failed to start service", error);
1377
+ shutdown("Failed to start service");
835
1378
  }
836
1379
  //# sourceMappingURL=cli.mjs.map