@mcesystems/apple-kit 1.0.10 → 1.0.22

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