Skip to content

Commit 40888ae

Browse files
address comments
1 parent 91cb05d commit 40888ae

2 files changed

Lines changed: 69 additions & 20 deletions

File tree

docs/reference/scripts.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ appium driver run xcuitest <script-name>
2121
|`tunnel-creation --udid=<device-udid>` or `-u <device-udid>`|Creates a tunnel for a specific iOS device with the given UDID|
2222
|`tunnel-creation --packet-stream-base-port=<port>`|Specifies the base port for packet stream servers (default: 50000)|
2323
|`tunnel-creation --tunnel-registry-port=<port>`|Specifies the port for the tunnel registry server (default: 42314)|
24+
|`tunnel-creation --appletv-device-id=<identifier>`|When adding an Apple TV over WiFi, tunnel only this device (use the identifier printed by `pair-appletv`). Omit to use the first discovered paired device.|
2425
|`download-wda-sim --outdir=/path/to/dir`|Download corresponding version's prebuilt WDA for iOS matched with the host machine architecture from [GitHub WebDriver release page](https://github.com/appium/WebDriverAgent/releases) into `--outdir` directory. The downloaded package name will be `WebDriverAgentRunner-Runner.app`.|
2526
|`download-wda-sim --platform=tvos --outdir=/path/to/dir`|Download corresponding version's prebuilt WDA for `--platform` into `--outdir` directory. If `--platform=tvos` is provided, the download module will be for tvOS (`WebDriverAgentRunner_tvOS-Runner.app`), otherwise the command will download iOS.|
2627
|`image-mounter mount --image <path> --manifest <path> --trustcache <path>`|Mount a Personalized Developer Disk Image on an iOS device. Requires paths to the .dmg image file, BuildManifest.plist, and .trustcache file. Requires the `appium-ios-remotexpc` optional dependency.|

scripts/tunnel-creation.mjs

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class TunnelCreator {
8484
packetStreamPort: result.packetStreamPort,
8585
connectionType: result.device.Properties.ConnectionType,
8686
productId: result.device.Properties.ProductID,
87-
createdAt: registry.tunnels[udid]?.createdAt ?? now,
87+
createdAt: now,
8888
lastUpdated: now,
8989
};
9090
}
@@ -99,7 +99,7 @@ class TunnelCreator {
9999
packetStreamPort: entry.packetStreamPort,
100100
connectionType: 'WiFi',
101101
productId: 0,
102-
createdAt: registry.tunnels[entry.udid]?.createdAt ?? now,
102+
createdAt: now,
103103
lastUpdated: now,
104104
};
105105
}
@@ -146,19 +146,9 @@ class TunnelCreator {
146146
}
147147
log.info(`Closing ${appletvResources.length} Apple TV tunnel(s)...`);
148148
await Promise.allSettled(
149-
appletvResources.map(async ({tunnel, packetStreamServer, tunnelService, udid}) => {
150-
try {
151-
await packetStreamServer.stop();
152-
if (_.isFunction(tunnel?.closer)) {
153-
await tunnel.closer();
154-
}
155-
if (tunnelService?.disconnect) {
156-
tunnelService.disconnect();
157-
}
158-
log.info(`Closed Apple TV tunnel for ${udid}`);
159-
} catch (err) {
160-
log.warn(`Failed to close Apple TV tunnel for ${udid}: ${err}`);
161-
}
149+
appletvResources.map(async (resource) => {
150+
await teardownAppleTVTunnelResource(resource, resource.udid);
151+
log.info(`Closed Apple TV tunnel for ${resource.udid}`);
162152
}),
163153
);
164154
this._appletvResources.length = 0;
@@ -300,9 +290,16 @@ class TunnelCreator {
300290
async setupAppleTVTunnels(specificDeviceId) {
301291
/** @type {AppleTVRegistryEntry[]} */
302292
const entries = [];
293+
/** @type {import('appium-ios-remotexpc').AppleTVTunnelService | null} */
294+
let tunnelService = null;
295+
/** @type {AppleTVTunnelConnection | null} */
296+
let tunnel = null;
297+
/** @type {import('appium-ios-remotexpc').PacketStreamServer | null} */
298+
let packetStreamServer = null;
299+
303300
try {
304301
log.info('Starting Apple TV tunnel (WiFi)...');
305-
const tunnelService = new AppleTVTunnelService();
302+
tunnelService = new AppleTVTunnelService();
306303
const result = await tunnelService.startTunnel(
307304
undefined,
308305
specificDeviceId ?? undefined,
@@ -315,10 +312,10 @@ class TunnelCreator {
315312
}
316313

317314
log.info(`Creating tunnel for Apple TV: ${deviceInfo.identifier}`);
318-
const tunnel = await TunnelManager.getTunnel(tlsSocket);
315+
tunnel = await TunnelManager.getTunnel(tlsSocket);
319316

320317
const packetStreamPort = this._packetStreamBasePort++;
321-
const packetStreamServer = new PacketStreamServer(packetStreamPort);
318+
packetStreamServer = new PacketStreamServer(packetStreamPort);
322319
await packetStreamServer.start();
323320

324321
const consumer = packetStreamServer.getPacketConsumer();
@@ -341,10 +338,49 @@ class TunnelCreator {
341338
packetStreamPort,
342339
});
343340
log.info(`✅ Apple TV tunnel ready for ${deviceInfo.identifier}`);
341+
return entries;
344342
} catch (err) {
345343
log.warn('Apple TV tunnel setup failed (ensure device is paired and on same network):', err?.message ?? err);
344+
// Clean up partially created resources so we don't leave a lingering WiFi connection
345+
if (tunnelService) {
346+
await teardownAppleTVTunnelResource(
347+
{tunnel, packetStreamServer, tunnelService},
348+
'partially created',
349+
);
350+
}
351+
return entries;
346352
}
347-
return entries;
353+
}
354+
}
355+
356+
/**
357+
* Tears down a single Apple TV tunnel resource (packet stream server, tunnel, tunnel service).
358+
* Each step runs in its own try/catch so one failure does not skip the rest.
359+
* @param {AppleTVTunnelTeardownInput} resource
360+
* @param {string} [label] - Label for log messages (e.g. device udid or 'partially created')
361+
*/
362+
async function teardownAppleTVTunnelResource(resource, label = 'Apple TV') {
363+
const {tunnel, packetStreamServer, tunnelService} = resource;
364+
try {
365+
if (packetStreamServer) {
366+
await packetStreamServer.stop();
367+
}
368+
} catch (err) {
369+
log.warn(`Failed to stop packet stream server for ${label}: ${err}`);
370+
}
371+
try {
372+
if (_.isFunction(tunnel?.closer)) {
373+
await tunnel.closer();
374+
}
375+
} catch (err) {
376+
log.warn(`Failed to close tunnel for ${label}: ${err}`);
377+
}
378+
try {
379+
if (tunnelService?.disconnect) {
380+
tunnelService.disconnect();
381+
}
382+
} catch (err) {
383+
log.warn(`Failed to disconnect tunnel service for ${label}: ${err}`);
348384
}
349385
}
350386

@@ -436,6 +472,10 @@ async function main() {
436472
}
437473
return port;
438474
},
475+
)
476+
.option(
477+
'--appletv-device-id <identifier>',
478+
'Apple TV device identifier to tunnel (from pair-appletv); omit to use first discovered paired device',
439479
);
440480

441481
program.parse(process.argv);
@@ -486,7 +526,7 @@ async function main() {
486526

487527
// Automatically add paired Apple TV(s) over WiFi when available
488528
/** @type {AppleTVRegistryEntry[]} */
489-
const appletvEntries = await tunnelCreator.setupAppleTVTunnels();
529+
const appletvEntries = await tunnelCreator.setupAppleTVTunnels(options.appletvDeviceId);
490530

491531
const registry = await tunnelCreator.updateTunnelRegistry(usbResults, appletvEntries);
492532
const totalTunnels = Object.keys(registry.tunnels).length;
@@ -540,6 +580,14 @@ await main();
540580
* @property {() => Promise<void>} [closer]
541581
*/
542582

583+
/**
584+
* @typedef {Object} AppleTVTunnelTeardownInput
585+
* Input for teardown of an Apple TV tunnel (full or partially created). All fields may be null if not yet created.
586+
* @property {AppleTVTunnelConnection | null} [tunnel]
587+
* @property {import('appium-ios-remotexpc').PacketStreamServer | null} [packetStreamServer]
588+
* @property {import('appium-ios-remotexpc').AppleTVTunnelService | null} [tunnelService]
589+
*/
590+
543591
/**
544592
* @typedef {Object} AppleTVTunnelResource
545593
* Resource handle for cleanup of a single Apple TV (WiFi) tunnel.

0 commit comments

Comments
 (0)