@mcesystems/apple-kit 1.0.10 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -806,6 +806,134 @@ function logTask(message) {
806
806
  debugTask(message);
807
807
  }
808
808
 
809
+ // src/utils/portManager.ts
810
+ import { createServer } from "node:net";
811
+ var PortManager = class {
812
+ allocations = /* @__PURE__ */ new Map();
813
+ basePort;
814
+ maxPortRange;
815
+ /**
816
+ * Create a new PortManager
817
+ *
818
+ * @param basePort Starting port number for allocation (default: 30000)
819
+ * @param maxPortRange Maximum range of ports to search (default: 1000)
820
+ */
821
+ constructor(basePort = 3e4, maxPortRange = 1e3) {
822
+ this.basePort = basePort;
823
+ this.maxPortRange = maxPortRange;
824
+ }
825
+ /**
826
+ * Find an available port starting from the given port number
827
+ */
828
+ async findAvailablePort(startPort) {
829
+ const maxPort = this.basePort + this.maxPortRange;
830
+ for (let port = startPort; port < maxPort; port++) {
831
+ const isAllocated = Array.from(this.allocations.values()).some(
832
+ (a) => a.localPort === port
833
+ );
834
+ if (isAllocated) {
835
+ continue;
836
+ }
837
+ const isAvailable = await this.isPortAvailable(port);
838
+ if (isAvailable) {
839
+ return port;
840
+ }
841
+ }
842
+ throw new Error(
843
+ `No available ports found in range ${startPort}-${maxPort}`
844
+ );
845
+ }
846
+ /**
847
+ * Check if a port is available for binding
848
+ */
849
+ isPortAvailable(port) {
850
+ return new Promise((resolve) => {
851
+ const server = createServer();
852
+ server.once("error", () => {
853
+ resolve(false);
854
+ });
855
+ server.once("listening", () => {
856
+ server.close(() => {
857
+ resolve(true);
858
+ });
859
+ });
860
+ server.listen(port, "127.0.0.1");
861
+ });
862
+ }
863
+ /**
864
+ * Allocate a local port for a logical port
865
+ *
866
+ * @param logicalPort The logical port number (USB hub position)
867
+ * @param devicePort The device port to forward to
868
+ * @returns The allocated port info
869
+ */
870
+ async allocate(logicalPort, devicePort) {
871
+ const existing = this.allocations.get(logicalPort);
872
+ if (existing) {
873
+ logTask(`Reusing existing port allocation for logical port ${logicalPort}: ${existing.localPort}`);
874
+ return existing;
875
+ }
876
+ const preferredPort = this.basePort + logicalPort;
877
+ let localPort;
878
+ const preferredAvailable = await this.isPortAvailable(preferredPort);
879
+ if (preferredAvailable) {
880
+ localPort = preferredPort;
881
+ } else {
882
+ localPort = await this.findAvailablePort(this.basePort);
883
+ }
884
+ const allocation = {
885
+ logicalPort,
886
+ localPort,
887
+ devicePort
888
+ };
889
+ this.allocations.set(logicalPort, allocation);
890
+ logTask(`Allocated port ${localPort} for logical port ${logicalPort} -> device port ${devicePort}`);
891
+ return allocation;
892
+ }
893
+ /**
894
+ * Release the port allocation for a logical port
895
+ */
896
+ release(logicalPort) {
897
+ const allocation = this.allocations.get(logicalPort);
898
+ if (allocation) {
899
+ logTask(`Released port ${allocation.localPort} for logical port ${logicalPort}`);
900
+ this.allocations.delete(logicalPort);
901
+ }
902
+ }
903
+ /**
904
+ * Get the current allocation for a logical port
905
+ */
906
+ getAllocation(logicalPort) {
907
+ return this.allocations.get(logicalPort);
908
+ }
909
+ /**
910
+ * Get all current allocations
911
+ */
912
+ getAllAllocations() {
913
+ return Array.from(this.allocations.values());
914
+ }
915
+ /**
916
+ * Release all allocations
917
+ */
918
+ releaseAll() {
919
+ logTask(`Releasing all port allocations (${this.allocations.size} ports)`);
920
+ this.allocations.clear();
921
+ }
922
+ /**
923
+ * Check if a logical port has an allocation
924
+ */
925
+ hasAllocation(logicalPort) {
926
+ return this.allocations.has(logicalPort);
927
+ }
928
+ };
929
+ var sharedPortManager = null;
930
+ function getSharedPortManager() {
931
+ if (!sharedPortManager) {
932
+ sharedPortManager = new PortManager();
933
+ }
934
+ return sharedPortManager;
935
+ }
936
+
809
937
  // src/logic/actions/device.ts
810
938
  import { exec as execCallback } from "node:child_process";
811
939
  import { existsSync } from "node:fs";
@@ -1203,51 +1331,107 @@ async function launchAppWithPymobiledevice3(bundleId, args, udid) {
1203
1331
  }
1204
1332
 
1205
1333
  // src/logic/actions/proxy.ts
1206
- async function startPortForward(localPort, devicePort, udid) {
1207
- logTask(`Starting port forward ${localPort} -> ${devicePort} for device ${udid}`);
1208
- const result = await runIDeviceTool("iproxy", [
1209
- localPort.toString(),
1210
- devicePort.toString(),
1211
- "-u",
1212
- udid
1213
- ]);
1214
- return {
1215
- localPort,
1216
- devicePort,
1217
- ...result
1218
- };
1219
- }
1220
- async function startPortForwardAsync(localPort, devicePort, udid, _timeout = 5e3) {
1221
- const result = await startPortForward(localPort, devicePort, udid);
1222
- await new Promise((resolve) => setTimeout(resolve, 500));
1223
- if (result.stdout.includes("error") || result.stderr.includes("error")) {
1224
- throw new Error("Port forwarding failed to start");
1334
+ import { spawn } from "node:child_process";
1335
+ import { join as join2 } from "node:path";
1336
+ function getResourcesBinPath2() {
1337
+ const binPath = process.env.IDeviceBinPath;
1338
+ if (!binPath) {
1339
+ throw new Error("IDeviceBinPath is not set");
1225
1340
  }
1226
- return result;
1341
+ return binPath;
1342
+ }
1343
+ function startPortForward(localPort, devicePort, udid, startupTimeout = 500) {
1344
+ return new Promise((resolve, reject) => {
1345
+ logTask(`Starting port forward ${localPort} -> ${devicePort} for device ${udid}`);
1346
+ const binPath = getResourcesBinPath2();
1347
+ const toolPath = join2(binPath, `iproxy${process.platform === "win32" ? ".exe" : ""}`);
1348
+ const child = spawn(
1349
+ toolPath,
1350
+ [localPort.toString(), devicePort.toString(), "-u", udid],
1351
+ {
1352
+ cwd: binPath,
1353
+ windowsHide: true,
1354
+ stdio: ["ignore", "pipe", "pipe"]
1355
+ }
1356
+ );
1357
+ let hasResolved = false;
1358
+ let stderrOutput = "";
1359
+ child.stderr?.on("data", (data) => {
1360
+ const msg = data.toString();
1361
+ stderrOutput += msg;
1362
+ if (msg.toLowerCase().includes("error") && !hasResolved) {
1363
+ hasResolved = true;
1364
+ child.kill();
1365
+ reject(new Error(`Port forwarding failed: ${msg}`));
1366
+ }
1367
+ });
1368
+ child.on("error", (error) => {
1369
+ if (!hasResolved) {
1370
+ hasResolved = true;
1371
+ reject(new Error(`Failed to start iproxy: ${error.message}`));
1372
+ }
1373
+ });
1374
+ child.on("exit", (code) => {
1375
+ if (!hasResolved) {
1376
+ hasResolved = true;
1377
+ if (code !== 0 && code !== null) {
1378
+ reject(new Error(`iproxy exited with code ${code}: ${stderrOutput}`));
1379
+ }
1380
+ }
1381
+ });
1382
+ setTimeout(() => {
1383
+ if (!hasResolved) {
1384
+ hasResolved = true;
1385
+ logTask(`Port forward started: ${localPort} -> ${devicePort} for device ${udid}`);
1386
+ resolve({
1387
+ result: {
1388
+ localPort,
1389
+ devicePort
1390
+ },
1391
+ process: child
1392
+ });
1393
+ }
1394
+ }, startupTimeout);
1395
+ });
1227
1396
  }
1228
- async function closePortForward(udid) {
1229
- logTask(`Closing port forward for device ${udid}`);
1230
- await runIDeviceTool("iproxy", ["-u", udid, "-c"]);
1397
+ function killPortForwardProcess(process2) {
1398
+ if (process2 && !process2.killed) {
1399
+ logTask("Killing port forward process");
1400
+ process2.kill();
1401
+ }
1231
1402
  }
1232
1403
 
1233
1404
  // src/logic/appleDeviceKit.ts
1234
1405
  var AppleDeviceKit = class {
1235
- constructor(udid, port) {
1236
- this.port = port;
1406
+ constructor(udid, logicalPort) {
1407
+ this.logicalPort = logicalPort;
1237
1408
  this.deviceId = udid;
1238
- logInfo(`AppleDeviceKit initialized for device: ${this.deviceId}`);
1409
+ logInfo(`AppleDeviceKit initialized for device: ${this.deviceId}, logical port: ${this.logicalPort}`);
1239
1410
  }
1240
1411
  deviceId;
1412
+ proxyProcess = null;
1413
+ portAllocation = null;
1414
+ isDisposed = false;
1415
+ /**
1416
+ * Throws if the kit has been disposed
1417
+ */
1418
+ ensureNotDisposed() {
1419
+ if (this.isDisposed) {
1420
+ throw new Error("AppleDeviceKit has been disposed");
1421
+ }
1422
+ }
1241
1423
  /**
1242
1424
  * Get detailed device information
1243
1425
  */
1244
1426
  async getDeviceInfo() {
1427
+ this.ensureNotDisposed();
1245
1428
  return getDeviceInfo(this.deviceId);
1246
1429
  }
1247
1430
  /**
1248
1431
  * Check if device is paired/trusted with this computer
1249
1432
  */
1250
1433
  async isPaired() {
1434
+ this.ensureNotDisposed();
1251
1435
  return isPaired(this.deviceId);
1252
1436
  }
1253
1437
  /**
@@ -1258,6 +1442,7 @@ var AppleDeviceKit = class {
1258
1442
  * @param pollInterval Poll interval in milliseconds (default: 1000)
1259
1443
  */
1260
1444
  async waitForPairing(timeout = 12e4, pollInterval = 1e3) {
1445
+ this.ensureNotDisposed();
1261
1446
  return waitForPairing(this.deviceId, timeout, pollInterval);
1262
1447
  }
1263
1448
  /**
@@ -1265,6 +1450,7 @@ var AppleDeviceKit = class {
1265
1450
  * User must accept the trust dialog on the device
1266
1451
  */
1267
1452
  async pair() {
1453
+ this.ensureNotDisposed();
1268
1454
  return pair(this.deviceId);
1269
1455
  }
1270
1456
  /**
@@ -1281,12 +1467,14 @@ var AppleDeviceKit = class {
1281
1467
  * @returns true if device is now trusted
1282
1468
  */
1283
1469
  async trustDevice(timeout = 6e4, onWaitingForTrust) {
1470
+ this.ensureNotDisposed();
1284
1471
  return trustDevice(this.deviceId, timeout, onWaitingForTrust);
1285
1472
  }
1286
1473
  /**
1287
1474
  * Unpair/untrust the device
1288
1475
  */
1289
1476
  async unpair() {
1477
+ this.ensureNotDisposed();
1290
1478
  return unpair(this.deviceId);
1291
1479
  }
1292
1480
  /**
@@ -1295,6 +1483,7 @@ var AppleDeviceKit = class {
1295
1483
  * @param ipaPath Path to the IPA file
1296
1484
  */
1297
1485
  async installApp(ipaPath) {
1486
+ this.ensureNotDisposed();
1298
1487
  installApp(ipaPath, this.deviceId);
1299
1488
  }
1300
1489
  /**
@@ -1303,6 +1492,7 @@ var AppleDeviceKit = class {
1303
1492
  * @param bundleId Application bundle identifier
1304
1493
  */
1305
1494
  async uninstallApp(bundleId) {
1495
+ this.ensureNotDisposed();
1306
1496
  uninstallApp(bundleId, this.deviceId);
1307
1497
  }
1308
1498
  /**
@@ -1311,12 +1501,14 @@ var AppleDeviceKit = class {
1311
1501
  * @param bundleId Application bundle identifier
1312
1502
  */
1313
1503
  async isAppInstalled(bundleId) {
1504
+ this.ensureNotDisposed();
1314
1505
  return isAppInstalled(bundleId, this.deviceId);
1315
1506
  }
1316
1507
  /**
1317
1508
  * List all installed user applications
1318
1509
  */
1319
1510
  async listApps() {
1511
+ this.ensureNotDisposed();
1320
1512
  return listApps(this.deviceId);
1321
1513
  }
1322
1514
  /**
@@ -1326,24 +1518,62 @@ var AppleDeviceKit = class {
1326
1518
  * @param args Application arguments
1327
1519
  */
1328
1520
  async launchApp(bundleId, args = []) {
1521
+ this.ensureNotDisposed();
1329
1522
  return launchApp(bundleId, args, this.deviceId);
1330
1523
  }
1331
1524
  /**
1332
1525
  * Start port forwarding and wait for it to be ready.
1333
- * we need port forwarding to be able to connect to the device from the computer
1526
+ * The local port is automatically allocated based on the logical port.
1527
+ *
1528
+ * We need port forwarding to be able to connect to the device from the computer
1334
1529
  * and communicate with it using the local port.
1335
1530
  *
1336
- * @param localPort Local port to listen on
1531
+ * Note: Only one port forward can be active at a time per kit instance.
1532
+ * Starting a new port forward will kill any existing one.
1533
+ *
1337
1534
  * @param devicePort Device port to forward to
1338
- * @param _timeout Timeout in milliseconds (reserved for future use)
1535
+ * @param startupTimeout Time to wait for proxy to start (ms)
1536
+ * @returns The port forward result with the allocated local port
1537
+ */
1538
+ async startPortForwardAsync(devicePort, startupTimeout = 500) {
1539
+ this.ensureNotDisposed();
1540
+ this.killProxyProcess();
1541
+ const portManager = getSharedPortManager();
1542
+ this.portAllocation = await portManager.allocate(this.logicalPort, devicePort);
1543
+ const { result, process: process2 } = await startPortForward(
1544
+ this.portAllocation.localPort,
1545
+ devicePort,
1546
+ this.deviceId,
1547
+ startupTimeout
1548
+ );
1549
+ this.proxyProcess = process2;
1550
+ return result;
1551
+ }
1552
+ /**
1553
+ * Kill the current port forwarding process if running
1339
1554
  */
1340
- async startPortForwardAsync(localPort, devicePort, _timeout = 5e3) {
1341
- return startPortForwardAsync(localPort, devicePort, this.deviceId);
1555
+ killProxyProcess() {
1556
+ if (this.proxyProcess) {
1557
+ killPortForwardProcess(this.proxyProcess);
1558
+ this.proxyProcess = null;
1559
+ }
1560
+ }
1561
+ /**
1562
+ * Close the current port forwarding session and release the port
1563
+ */
1564
+ closePortForward() {
1565
+ this.killProxyProcess();
1566
+ if (this.portAllocation) {
1567
+ const portManager = getSharedPortManager();
1568
+ portManager.release(this.logicalPort);
1569
+ this.portAllocation = null;
1570
+ }
1342
1571
  }
1343
1572
  /**
1344
1573
  * Get the activation state of the device
1345
1574
  */
1346
1575
  async getActivationState() {
1576
+ this.ensureNotDisposed();
1347
1577
  return getActivationState(this.deviceId);
1348
1578
  }
1349
1579
  /**
@@ -1357,6 +1587,7 @@ var AppleDeviceKit = class {
1357
1587
  * precondition: the device must be paired and trusted
1358
1588
  */
1359
1589
  async activate() {
1590
+ this.ensureNotDisposed();
1360
1591
  return activate(this.deviceId);
1361
1592
  }
1362
1593
  /**
@@ -1366,16 +1597,60 @@ var AppleDeviceKit = class {
1366
1597
  return this.deviceId;
1367
1598
  }
1368
1599
  /**
1369
- * Get the logical port number
1600
+ * Get the logical port number (USB hub position)
1601
+ */
1602
+ getLogicalPort() {
1603
+ return this.logicalPort;
1604
+ }
1605
+ /**
1606
+ * Get the currently allocated local port for forwarding
1607
+ * Returns undefined if no port forward is active
1608
+ */
1609
+ getAllocatedPort() {
1610
+ return this.portAllocation?.localPort;
1611
+ }
1612
+ /**
1613
+ * Get the current port allocation info
1614
+ */
1615
+ getPortAllocation() {
1616
+ return this.portAllocation;
1617
+ }
1618
+ /**
1619
+ * Check if this kit has been disposed
1370
1620
  */
1371
- getPort() {
1372
- return this.port;
1621
+ get disposed() {
1622
+ return this.isDisposed;
1373
1623
  }
1374
- async closePortForward() {
1375
- return closePortForward(this.deviceId);
1624
+ /**
1625
+ * Check if a port forward is currently active
1626
+ */
1627
+ get hasActivePortForward() {
1628
+ return this.proxyProcess !== null && !this.proxyProcess.killed;
1629
+ }
1630
+ /**
1631
+ * Dispose of the kit and clean up all resources.
1632
+ * This will kill any running proxy processes and release port allocations.
1633
+ *
1634
+ * After calling dispose(), the kit instance should not be used.
1635
+ */
1636
+ dispose() {
1637
+ if (this.isDisposed) {
1638
+ return;
1639
+ }
1640
+ logInfo(`Disposing AppleDeviceKit for device: ${this.deviceId}`);
1641
+ this.closePortForward();
1642
+ this.isDisposed = true;
1643
+ }
1644
+ /**
1645
+ * Symbol.dispose implementation for using with `using` keyword (TypeScript 5.2+)
1646
+ */
1647
+ [Symbol.dispose]() {
1648
+ this.dispose();
1376
1649
  }
1377
1650
  };
1378
1651
  export {
1379
- AppleDeviceKit
1652
+ AppleDeviceKit,
1653
+ PortManager,
1654
+ getSharedPortManager
1380
1655
  };
1381
1656
  //# sourceMappingURL=index.mjs.map