appium-ios-remotexpc 0.37.0 → 0.37.2
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/CHANGELOG.md +8 -0
- package/README.md +28 -5
- package/package.json +9 -8
- package/scripts/pair-appletv.mjs +39 -0
- package/scripts/{start-appletv-tunnel.ts → start-appletv-tunnel.mjs} +55 -32
- package/scripts/{test-tunnel-creation.ts → tunnel-creation.mjs} +74 -94
- package/scripts/pair-appletv.ts +0 -78
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## [0.37.2](https://github.com/appium/appium-ios-remotexpc/compare/v0.37.1...v0.37.2) (2026-03-22)
|
|
2
|
+
|
|
3
|
+
### Miscellaneous Chores
|
|
4
|
+
|
|
5
|
+
* Migrate scripts to ESM/.mjs ([#173](https://github.com/appium/appium-ios-remotexpc/issues/173)) ([8d6053b](https://github.com/appium/appium-ios-remotexpc/commit/8d6053b6bfde353296905daf345b8e15d4307873))
|
|
6
|
+
|
|
7
|
+
## [0.37.1](https://github.com/appium/appium-ios-remotexpc/compare/v0.37.0...v0.37.1) (2026-03-21)
|
|
8
|
+
|
|
1
9
|
## [0.37.0](https://github.com/appium/appium-ios-remotexpc/compare/v0.36.0...v0.37.0) (2026-03-21)
|
|
2
10
|
|
|
3
11
|
### Features
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ This library provides functionality for:
|
|
|
11
11
|
|
|
12
12
|
- Remote XPC (Cross Process Communication) with iOS devices
|
|
13
13
|
- Lockdown communication
|
|
14
|
-
- USB
|
|
14
|
+
- Device multiplexing via **usbmuxd** (lists USB **and** WiFi-attached devices; see below)
|
|
15
15
|
- Property list (plist) handling
|
|
16
16
|
- IPv6 tunneling services to iOS devices using TUN/TAP interfaces
|
|
17
17
|
- System log access
|
|
@@ -32,7 +32,7 @@ npm install appium-ios-remotexpc
|
|
|
32
32
|
## Features
|
|
33
33
|
|
|
34
34
|
- **Plist Handling**: Encode, decode, parse, and create property lists for iOS device communication.
|
|
35
|
-
- **
|
|
35
|
+
- **Device communication over usbmux / usbmuxd**: The system **usbmuxd** daemon exposes a single device list that includes machines plugged in over **USB** and, when pairing and wireless sync are set up, the same iPhone/iPad **over WiFi**. WiFi entries are marked **`ConnectionType: Network`** (USB entries use `ConnectionType: USB`). This library connects through usbmuxd the same way for both; the tunnel path is unchanged (lockdown → CoreDeviceProxy → TUN/TAP → Remote XPC).
|
|
36
36
|
- **Remote XPC**: Establish Remote XPC connections with iOS devices.
|
|
37
37
|
- **Service Architecture**: Connect to various iOS services:
|
|
38
38
|
- System Log Service: Access device logs
|
|
@@ -130,6 +130,20 @@ const remoteXPC = await TunnelManager.createRemoteXPCConnection(
|
|
|
130
130
|
const services = remoteXPC.getServices();
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
### iPhone / iPad over WiFi (usbmuxd “network” devices)
|
|
134
|
+
|
|
135
|
+
**usbmuxd** (the multiplexer daemon, e.g. on macOS) does **not** only list USB devices: once a device is paired with the host and wireless sync / lockdown-over-WiFi is enabled, **the same daemon’s device list includes that device as attached over WiFi**. In plist responses from `ListDevices`, those rows carry **`ConnectionType: Network`** (and a distinct `DeviceID` from any USB listing for the same physical device).
|
|
136
|
+
|
|
137
|
+
There is no separate “WiFi API” in this library: call `createUsbmux()` → `listDevices()` (or any other client that queries **usbmuxd**) and use the returned **`DeviceID`** and UDID with `createLockdownServiceByUDID` and the tunnel steps in the previous section—identical to USB.
|
|
138
|
+
|
|
139
|
+
**Typical host-side setup:**
|
|
140
|
+
|
|
141
|
+
1. Pair the device with this Mac and tap **Trust** on the device if prompted.
|
|
142
|
+
2. Allow the device to connect over WiFi (e.g. in Finder under the device, enable **Show this [device] when on WiFi**, or use Xcode **Devices and Simulators** with the equivalent option so lockdown can reach the device without USB).
|
|
143
|
+
3. Confirm **usbmuxd** reports the device with **`ConnectionType: Network`**—for example by logging the result of `listDevices()` from this library, or by checking another usbmuxd client’s device list while the device is on the same network and not on USB.
|
|
144
|
+
|
|
145
|
+
For an end-to-end tunnel smoke test with the tunnel registry HTTP API, use `npm run tunnel-creation` or `npm run test:tunnel-creation` (see `scripts/test-tunnel-creation.ts`), usually with **sudo** for TUN/TAP.
|
|
146
|
+
|
|
133
147
|
### Apple TV / tvOS over WiFi
|
|
134
148
|
|
|
135
149
|
Apple TV and tvOS devices over WiFi are supported. The following symbols are part of the public API and are intended for external use (e.g. by the Appium XCUITest driver):
|
|
@@ -182,10 +196,19 @@ All pull requests must pass these checks before merging. The workflows are defin
|
|
|
182
196
|
- `npm run format` - Run prettier
|
|
183
197
|
- `npm run lint:fix` - Run ESLint with auto-fix
|
|
184
198
|
- `npm test` - Run tests (requires sudo privileges for tunneling)
|
|
185
|
-
|
|
199
|
+
|
|
200
|
+
CLI helpers under `scripts/` are ESM (`.mjs`) and load the library via the package entrypoint. Run `npm run build` before using them so `appium-ios-remotexpc` resolves to `build/`.
|
|
201
|
+
|
|
202
|
+
- `npm run tunnel-creation` / `npm run test:tunnel-creation` — Create USB tunnels and start the tunnel registry HTTP API (requires `sudo`)
|
|
203
|
+
- `npm run test:tunnel-creation:lsof` — Same as above with `--keep-open` (for inspecting open sockets)
|
|
204
|
+
- `npm run pair-appletv` — Pair an Apple TV over WiFi for Remote XPC (requires `sudo`)
|
|
205
|
+
- `npm run start-appletv-tunnel` — Start an Apple TV WiFi tunnel and tunnel registry (requires `sudo`)
|
|
206
|
+
|
|
207
|
+
Pass `--help` after `--` to any of these npm scripts to see CLI flags (for example: `npm run pair-appletv -- --help`).
|
|
186
208
|
|
|
187
209
|
## Project Structure
|
|
188
210
|
|
|
211
|
+
- `/scripts` - Optional CLI helpers (ESM `.mjs`) for tunnels and Apple TV pairing; use via `npm run` entries under [Scripts](#scripts)
|
|
189
212
|
- `/src` - Source code
|
|
190
213
|
- `/lib` - Core libraries
|
|
191
214
|
- `/lockdown` - Device lockdown protocol
|
|
@@ -193,7 +216,7 @@ All pull requests must pass these checks before merging. The workflows are defin
|
|
|
193
216
|
- `/plist` - Property list processing
|
|
194
217
|
- `/remote-xpc` - XPC connection handling
|
|
195
218
|
- `/tunnel` - Tunneling implementation with tuntap integration
|
|
196
|
-
- `/usbmux` - USB
|
|
219
|
+
- `/usbmux` - usbmuxd client (USB and WiFi-listed devices)
|
|
197
220
|
- `/services` - Service implementations
|
|
198
221
|
- `/ios`
|
|
199
222
|
- `/diagnostic-service` - Device diagnostics
|
|
@@ -208,7 +231,7 @@ npm test
|
|
|
208
231
|
```
|
|
209
232
|
|
|
210
233
|
Note: Integration tests require:
|
|
211
|
-
- Physical iOS devices connected
|
|
234
|
+
- Physical iOS devices connected (USB and/or **WiFi** if the device is paired and visible to usbmux as `Network`)
|
|
212
235
|
- Sudo privileges for tunnel creation
|
|
213
236
|
- Device trust established
|
|
214
237
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appium-ios-remotexpc",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
4
4
|
"main": "build/src/index.js",
|
|
5
5
|
"types": "build/src/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"clean:build": "rimraf ./build",
|
|
19
19
|
"build:es": "tsc",
|
|
20
20
|
"build": "run-s clean:* build:*",
|
|
21
|
-
"lint": "eslint src
|
|
21
|
+
"lint": "eslint src scripts --quiet",
|
|
22
22
|
"prepare": "husky && npm run build",
|
|
23
23
|
"format": "prettier --write \"{src,test}/**/*.{ts,tsx}\"",
|
|
24
24
|
"format:check": "prettier --check \"{src,test}/**/*.{ts,tsx}\"",
|
|
25
|
-
"lint:fix": "eslint src
|
|
25
|
+
"lint:fix": "eslint src scripts --fix",
|
|
26
26
|
"test": "mocha test/integration/**/*.ts",
|
|
27
27
|
"test:all": "mocha -r tsx/cjs test/run-integration-tests.ts",
|
|
28
28
|
"test:tunnel": "mocha test/integration/tunnel-test.ts --exit --timeout 1m",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"test:mobile-config": "mocha test/integration/mobile-config-test.ts --exit --timeout 1m",
|
|
34
34
|
"test:springboard": "mocha test/integration/springboard-service-test.ts --exit --timeout 1m",
|
|
35
35
|
"test:unit": "NODE_ENV=test mocha 'test/unit/**/*.ts' --exit --timeout 2m",
|
|
36
|
-
"tunnel-creation": "sudo
|
|
37
|
-
"pair-appletv": "sudo
|
|
38
|
-
"start-appletv-tunnel": "sudo
|
|
36
|
+
"tunnel-creation": "sudo node scripts/tunnel-creation.mjs",
|
|
37
|
+
"pair-appletv": "sudo node scripts/pair-appletv.mjs",
|
|
38
|
+
"start-appletv-tunnel": "sudo node scripts/start-appletv-tunnel.mjs",
|
|
39
39
|
"test:webinspector": "mocha test/integration/webinspector-test.ts --exit --timeout 1m",
|
|
40
40
|
"test:misagent": "mocha test/integration/misagent-service-test.ts --exit --timeout 1m",
|
|
41
41
|
"test:afc": "mocha test/integration/afc-test.ts --exit --timeout 1m",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"test:dvt:network-monitor": "mocha test/integration/dvt_instruments/network-monitor-test.ts --exit --timeout 1m",
|
|
56
56
|
"test:dvt:process-control": "mocha test/integration/process-control-test.ts --exit --timeout 1m",
|
|
57
57
|
"test:testmanagerd": "mocha test/integration/testmanagerd-test.ts --exit --timeout 2m",
|
|
58
|
-
"test:tunnel-creation": "sudo
|
|
59
|
-
"test:tunnel-creation:lsof": "sudo
|
|
58
|
+
"test:tunnel-creation": "sudo node scripts/tunnel-creation.mjs",
|
|
59
|
+
"test:tunnel-creation:lsof": "sudo node scripts/tunnel-creation.mjs --keep-open"
|
|
60
60
|
},
|
|
61
61
|
"keywords": [],
|
|
62
62
|
"author": "Appium Contributors",
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
"@xmldom/xmldom": "^0.9.8",
|
|
103
103
|
"appium-ios-tuntap": "^0.x",
|
|
104
104
|
"axios": "^1.12.0",
|
|
105
|
+
"commander": "^14.0.1",
|
|
105
106
|
"dnssd": "^0.x",
|
|
106
107
|
"minimatch": "^10.1.1",
|
|
107
108
|
"node-devicectl": "^1.2.0",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Pair Apple TV / tvOS devices over WiFi for Remote XPC tunnels.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npm run pair-appletv -- [options]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from '@appium/support';
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import { AppleTVPairingService, UserInputService } from 'appium-ios-remotexpc';
|
|
12
|
+
|
|
13
|
+
const log = logger.getLogger('AppleTVPairing');
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
const program = new Command();
|
|
17
|
+
program
|
|
18
|
+
.name('pair-appletv')
|
|
19
|
+
.description('Pair Apple TV / tvOS devices over WiFi for Remote XPC tunnels')
|
|
20
|
+
.option(
|
|
21
|
+
'-d, --device <selector>',
|
|
22
|
+
'Device selector: name, identifier (e.g. AA:BB:CC:DD:EE:FF), or index (0, 1, …)',
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
program.parse(process.argv);
|
|
26
|
+
const options = program.opts();
|
|
27
|
+
|
|
28
|
+
const userInput = new UserInputService();
|
|
29
|
+
const pairingService = new AppleTVPairingService(userInput);
|
|
30
|
+
const result = await pairingService.discoverAndPair(options.device);
|
|
31
|
+
|
|
32
|
+
if (result.success) {
|
|
33
|
+
log.info(`Pairing successful! Record saved to: ${result.pairingFile}`);
|
|
34
|
+
} else {
|
|
35
|
+
throw result.error ?? new Error('Pairing failed');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await main();
|
|
@@ -1,43 +1,70 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import { getLogger } from '../src/lib/logger.js';
|
|
9
|
-
import type { TunnelConnection } from '../src/lib/tunnel/index.js';
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Start an Apple TV Remote XPC tunnel and expose the tunnel registry API.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logger } from '@appium/support';
|
|
7
|
+
import { Command } from 'commander';
|
|
10
8
|
import {
|
|
11
|
-
|
|
9
|
+
AppleTVTunnelService,
|
|
10
|
+
PacketStreamServer,
|
|
11
|
+
TunnelManager,
|
|
12
12
|
startTunnelRegistryServer,
|
|
13
|
-
} from '
|
|
13
|
+
} from 'appium-ios-remotexpc';
|
|
14
|
+
import { DEFAULT_TUNNEL_REGISTRY_PORT } from '../build/src/lib/tunnel/tunnel-registry-server.js';
|
|
14
15
|
|
|
15
|
-
const log = getLogger('WiFiTunnel');
|
|
16
|
+
const log = logger.getLogger('WiFiTunnel');
|
|
16
17
|
const PACKET_STREAM_PORT = 50100;
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
19
|
+
function parsePort(value) {
|
|
20
|
+
const port = Number.parseInt(value, 10);
|
|
21
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Invalid port: ${value}. Expected an integer between 1 and 65535.`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return port;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
const program = new Command();
|
|
31
|
+
program
|
|
32
|
+
.name('start-appletv-tunnel')
|
|
33
|
+
.description('Start an Apple TV WiFi tunnel and tunnel registry HTTP API')
|
|
34
|
+
.argument(
|
|
35
|
+
'[deviceIdentifier]',
|
|
36
|
+
'Optional Apple TV device identifier to target',
|
|
37
|
+
)
|
|
38
|
+
.option(
|
|
39
|
+
'--tunnel-registry-port <port>',
|
|
40
|
+
`Port for tunnel registry API (default: ${DEFAULT_TUNNEL_REGISTRY_PORT})`,
|
|
41
|
+
parsePort,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
program.parse(process.argv);
|
|
45
|
+
const options = program.opts();
|
|
46
|
+
const deviceIdentifier = program.args[0];
|
|
47
|
+
const registryPort =
|
|
48
|
+
options.tunnelRegistryPort ?? DEFAULT_TUNNEL_REGISTRY_PORT;
|
|
21
49
|
|
|
22
|
-
if (
|
|
50
|
+
if (deviceIdentifier) {
|
|
23
51
|
log.info(
|
|
24
|
-
`Starting Apple TV tunnel for specific device identifier: ${
|
|
52
|
+
`Starting Apple TV tunnel for specific device identifier: ${deviceIdentifier}`,
|
|
25
53
|
);
|
|
26
54
|
} else {
|
|
27
55
|
log.info('Starting Apple TV tunnel (will try all discovered devices)');
|
|
28
56
|
}
|
|
29
57
|
|
|
30
58
|
const tunnelService = new AppleTVTunnelService();
|
|
31
|
-
let tunnel
|
|
32
|
-
let tlsSocket
|
|
33
|
-
let deviceInfo
|
|
34
|
-
let packetStreamServer
|
|
59
|
+
let tunnel = null;
|
|
60
|
+
let tlsSocket = null;
|
|
61
|
+
let deviceInfo = null;
|
|
62
|
+
let packetStreamServer = null;
|
|
35
63
|
|
|
36
|
-
const cleanup = async (signal
|
|
64
|
+
const cleanup = async (signal) => {
|
|
37
65
|
log.warn(`\nCleaning up (${signal})...`);
|
|
38
66
|
|
|
39
67
|
try {
|
|
40
|
-
// Close packet stream server first
|
|
41
68
|
if (packetStreamServer) {
|
|
42
69
|
log.info('Closing packet stream server...');
|
|
43
70
|
await packetStreamServer.stop();
|
|
@@ -89,7 +116,7 @@ async function main(): Promise<void> {
|
|
|
89
116
|
log.info('Starting Apple TV tunnel...');
|
|
90
117
|
const result = await tunnelService.startTunnel(
|
|
91
118
|
undefined,
|
|
92
|
-
|
|
119
|
+
deviceIdentifier,
|
|
93
120
|
);
|
|
94
121
|
tlsSocket = result.socket;
|
|
95
122
|
deviceInfo = result.device;
|
|
@@ -101,13 +128,11 @@ async function main(): Promise<void> {
|
|
|
101
128
|
log.info('Creating tunnel with TunnelManager...');
|
|
102
129
|
tunnel = await TunnelManager.getTunnel(tlsSocket);
|
|
103
130
|
|
|
104
|
-
// Start packet stream server (same as iPhone tunnel)
|
|
105
131
|
let packetStreamPort = 0;
|
|
106
132
|
try {
|
|
107
133
|
packetStreamServer = new PacketStreamServer(PACKET_STREAM_PORT);
|
|
108
134
|
await packetStreamServer.start();
|
|
109
135
|
|
|
110
|
-
// Attach packet consumer to tunnel to receive packet data
|
|
111
136
|
const consumer = packetStreamServer.getPacketConsumer();
|
|
112
137
|
if (consumer && tunnel.addPacketConsumer) {
|
|
113
138
|
tunnel.addPacketConsumer(consumer);
|
|
@@ -123,7 +148,7 @@ async function main(): Promise<void> {
|
|
|
123
148
|
const now = Date.now();
|
|
124
149
|
const nowISOString = new Date().toISOString();
|
|
125
150
|
|
|
126
|
-
const registry
|
|
151
|
+
const registry = {
|
|
127
152
|
tunnels: {
|
|
128
153
|
[deviceInfo.identifier]: {
|
|
129
154
|
udid: deviceInfo.identifier,
|
|
@@ -144,7 +169,7 @@ async function main(): Promise<void> {
|
|
|
144
169
|
},
|
|
145
170
|
};
|
|
146
171
|
|
|
147
|
-
await startTunnelRegistryServer(registry);
|
|
172
|
+
await startTunnelRegistryServer(registry, registryPort);
|
|
148
173
|
|
|
149
174
|
log.info('=== TUNNEL ESTABLISHED ===');
|
|
150
175
|
log.info(`Tunnel Address: ${tunnel.Address}`);
|
|
@@ -154,7 +179,7 @@ async function main(): Promise<void> {
|
|
|
154
179
|
|
|
155
180
|
log.info('\n📁 Tunnel registry API:');
|
|
156
181
|
log.info(
|
|
157
|
-
` http://localhost:${
|
|
182
|
+
` http://localhost:${registryPort}/remotexpc/tunnels`,
|
|
158
183
|
);
|
|
159
184
|
log.info(' - GET /remotexpc/tunnels - List all tunnels');
|
|
160
185
|
log.info(
|
|
@@ -169,9 +194,7 @@ async function main(): Promise<void> {
|
|
|
169
194
|
} catch (error) {
|
|
170
195
|
log.error('Tunnel failed:', error);
|
|
171
196
|
throw error;
|
|
172
|
-
} finally {
|
|
173
|
-
await cleanup('Shutdown');
|
|
174
197
|
}
|
|
175
198
|
}
|
|
176
199
|
|
|
177
|
-
main();
|
|
200
|
+
await main();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* This script demonstrates the tunnel creation workflow for all connected devices
|
|
3
|
+
* Create lockdown + CoreDeviceProxy tunnels for connected USB devices and expose the tunnel registry API.
|
|
5
4
|
*/
|
|
5
|
+
|
|
6
6
|
import { logger } from '@appium/support';
|
|
7
|
-
import
|
|
7
|
+
import { Command } from 'commander';
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
PacketStreamServer,
|
|
@@ -12,26 +12,27 @@ import {
|
|
|
12
12
|
createLockdownServiceByUDID,
|
|
13
13
|
createUsbmux,
|
|
14
14
|
startCoreDeviceProxy,
|
|
15
|
-
} from '../src/index.js';
|
|
16
|
-
import type { SocketInfo, TunnelRegistry } from '../src/index.js';
|
|
17
|
-
import {
|
|
18
|
-
DEFAULT_TUNNEL_REGISTRY_PORT,
|
|
19
15
|
startTunnelRegistryServer,
|
|
20
|
-
} from '
|
|
21
|
-
import
|
|
16
|
+
} from 'appium-ios-remotexpc';
|
|
17
|
+
import { DEFAULT_TUNNEL_REGISTRY_PORT } from '../build/src/lib/tunnel/tunnel-registry-server.js';
|
|
22
18
|
|
|
23
19
|
const log = logger.getLogger('TunnelCreation');
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
|
|
21
|
+
function parsePort(value) {
|
|
22
|
+
const port = Number.parseInt(value, 10);
|
|
23
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Invalid port: ${value}. Expected an integer between 1 and 65535.`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return port;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function updateTunnelRegistry(results) {
|
|
30
32
|
const now = Date.now();
|
|
31
33
|
const nowISOString = new Date().toISOString();
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
const registry: TunnelRegistry = {
|
|
35
|
+
const registry = {
|
|
35
36
|
tunnels: {},
|
|
36
37
|
metadata: {
|
|
37
38
|
lastUpdated: nowISOString,
|
|
@@ -40,7 +41,6 @@ async function updateTunnelRegistry(
|
|
|
40
41
|
},
|
|
41
42
|
};
|
|
42
43
|
|
|
43
|
-
// Update tunnels
|
|
44
44
|
for (const result of results) {
|
|
45
45
|
if (result.success) {
|
|
46
46
|
const udid = result.device.Properties.SerialNumber;
|
|
@@ -58,39 +58,21 @@ async function updateTunnelRegistry(
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// Update metadata
|
|
62
61
|
registry.metadata = {
|
|
63
62
|
lastUpdated: nowISOString,
|
|
64
63
|
totalTunnels: Object.keys(registry.tunnels).length,
|
|
65
|
-
activeTunnels: Object.keys(registry.tunnels).length,
|
|
64
|
+
activeTunnels: Object.keys(registry.tunnels).length,
|
|
66
65
|
};
|
|
67
66
|
|
|
68
67
|
return registry;
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
const
|
|
72
|
-
const packetStreamServers: Map<string, PacketStreamServer> = new Map();
|
|
73
|
-
|
|
74
|
-
const deviceInfoMap: Map<
|
|
75
|
-
string,
|
|
76
|
-
{
|
|
77
|
-
udid: string;
|
|
78
|
-
address: string;
|
|
79
|
-
rsdPort?: number;
|
|
80
|
-
connectionType: string;
|
|
81
|
-
productId: number;
|
|
82
|
-
}
|
|
83
|
-
> = new Map();
|
|
70
|
+
const packetStreamServers = new Map();
|
|
84
71
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
* Setup cleanup handlers for graceful shutdown
|
|
88
|
-
*/
|
|
89
|
-
function setupCleanupHandlers(): void {
|
|
90
|
-
const cleanup = async (signal: string) => {
|
|
72
|
+
function setupCleanupHandlers() {
|
|
73
|
+
const cleanup = async (signal) => {
|
|
91
74
|
log.warn(`\nCleaning up (${signal})...`);
|
|
92
75
|
|
|
93
|
-
// Close all packet stream servers
|
|
94
76
|
if (packetStreamServers.size > 0) {
|
|
95
77
|
log.info(
|
|
96
78
|
`Closing ${packetStreamServers.size} packet stream server(s)...`,
|
|
@@ -111,7 +93,6 @@ function setupCleanupHandlers(): void {
|
|
|
111
93
|
log.info('Cleanup completed.');
|
|
112
94
|
};
|
|
113
95
|
|
|
114
|
-
// Handle various termination signals
|
|
115
96
|
process.on('SIGINT', async () => {
|
|
116
97
|
await cleanup('SIGINT (Ctrl+C)');
|
|
117
98
|
process.exit(0);
|
|
@@ -125,7 +106,6 @@ function setupCleanupHandlers(): void {
|
|
|
125
106
|
process.exit(0);
|
|
126
107
|
});
|
|
127
108
|
|
|
128
|
-
// Handle uncaught exceptions and unhandled rejections
|
|
129
109
|
process.on('uncaughtException', async (error) => {
|
|
130
110
|
log.error('Uncaught Exception:', error);
|
|
131
111
|
await cleanup('Uncaught Exception');
|
|
@@ -137,24 +117,7 @@ function setupCleanupHandlers(): void {
|
|
|
137
117
|
});
|
|
138
118
|
}
|
|
139
119
|
|
|
140
|
-
|
|
141
|
-
* Interface for tunnel result
|
|
142
|
-
*/
|
|
143
|
-
interface TunnelResult {
|
|
144
|
-
device: Device;
|
|
145
|
-
tunnel: {
|
|
146
|
-
Address: string;
|
|
147
|
-
RsdPort?: number;
|
|
148
|
-
};
|
|
149
|
-
packetStreamPort?: number;
|
|
150
|
-
success: boolean;
|
|
151
|
-
error?: string;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async function createTunnelForDevice(
|
|
155
|
-
device: Device,
|
|
156
|
-
tlsOptions: Partial<ConnectionOptions>,
|
|
157
|
-
): Promise<TunnelResult & { socket?: any; socketInfo?: SocketInfo }> {
|
|
120
|
+
async function createTunnelForDevice(device, tlsOptions, packetStreamBaseRef) {
|
|
158
121
|
const udid = device.Properties.SerialNumber;
|
|
159
122
|
|
|
160
123
|
try {
|
|
@@ -185,9 +148,9 @@ async function createTunnelForDevice(
|
|
|
185
148
|
`Tunnel created for address: ${tunnel.Address} with RsdPort: ${tunnel.RsdPort}`,
|
|
186
149
|
);
|
|
187
150
|
|
|
188
|
-
let packetStreamPort
|
|
151
|
+
let packetStreamPort;
|
|
189
152
|
try {
|
|
190
|
-
packetStreamPort =
|
|
153
|
+
packetStreamPort = packetStreamBaseRef.value++;
|
|
191
154
|
const packetStreamServer = new PacketStreamServer(packetStreamPort);
|
|
192
155
|
await packetStreamServer.start();
|
|
193
156
|
|
|
@@ -215,16 +178,6 @@ async function createTunnelForDevice(
|
|
|
215
178
|
socket.setNoDelay(true);
|
|
216
179
|
}
|
|
217
180
|
|
|
218
|
-
const deviceInfo = {
|
|
219
|
-
udid: device.Properties.SerialNumber,
|
|
220
|
-
address: tunnel.Address,
|
|
221
|
-
rsdPort: tunnel.RsdPort,
|
|
222
|
-
connectionType: device.Properties.ConnectionType,
|
|
223
|
-
productId: device.Properties.ProductID,
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
deviceInfoMap.set(device.Properties.SerialNumber, deviceInfo);
|
|
227
|
-
|
|
228
181
|
return {
|
|
229
182
|
device,
|
|
230
183
|
tunnel: {
|
|
@@ -261,14 +214,32 @@ async function createTunnelForDevice(
|
|
|
261
214
|
}
|
|
262
215
|
}
|
|
263
216
|
|
|
264
|
-
|
|
265
|
-
*/
|
|
266
|
-
async function main(): Promise<void> {
|
|
217
|
+
async function main() {
|
|
267
218
|
setupCleanupHandlers();
|
|
268
219
|
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
220
|
+
const program = new Command();
|
|
221
|
+
program
|
|
222
|
+
.name('tunnel-creation')
|
|
223
|
+
.description(
|
|
224
|
+
'Create tunnels for connected USB devices (lockdown + CoreDeviceProxy)',
|
|
225
|
+
)
|
|
226
|
+
.argument('[udid]', 'Optional device UDID (omit for all devices)')
|
|
227
|
+
.option('--udid <udid>', 'UDID of the device to create tunnel for')
|
|
228
|
+
.option('-k, --keep-open', 'Keep connections open for lsof inspection')
|
|
229
|
+
.option(
|
|
230
|
+
'--packet-stream-base-port <port>',
|
|
231
|
+
'Base port for packet stream servers (1-65535)',
|
|
232
|
+
parsePort,
|
|
233
|
+
)
|
|
234
|
+
.option(
|
|
235
|
+
'--tunnel-registry-port <port>',
|
|
236
|
+
`Port for tunnel registry API (default: ${DEFAULT_TUNNEL_REGISTRY_PORT})`,
|
|
237
|
+
parsePort,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
program.parse(process.argv);
|
|
241
|
+
const options = program.opts();
|
|
242
|
+
const specificUdid = options.udid ?? program.args[0] ?? undefined;
|
|
272
243
|
|
|
273
244
|
if (specificUdid) {
|
|
274
245
|
log.info(
|
|
@@ -278,16 +249,22 @@ async function main(): Promise<void> {
|
|
|
278
249
|
log.info('Starting tunnel creation test for all connected devices');
|
|
279
250
|
}
|
|
280
251
|
|
|
281
|
-
if (
|
|
252
|
+
if (options.keepOpen) {
|
|
282
253
|
log.info('Running in "keep connections open" mode for lsof inspection');
|
|
283
254
|
}
|
|
284
255
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
256
|
+
const tlsOptions = {
|
|
257
|
+
rejectUnauthorized: false,
|
|
258
|
+
minVersion: 'TLSv1.2',
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const packetStreamBaseRef = {
|
|
262
|
+
value: options.packetStreamBasePort ?? 50000,
|
|
263
|
+
};
|
|
264
|
+
const registryPort =
|
|
265
|
+
options.tunnelRegistryPort ?? DEFAULT_TUNNEL_REGISTRY_PORT;
|
|
290
266
|
|
|
267
|
+
try {
|
|
291
268
|
log.info('Connecting to usbmuxd...');
|
|
292
269
|
const usbmux = await createUsbmux();
|
|
293
270
|
|
|
@@ -331,10 +308,14 @@ async function main(): Promise<void> {
|
|
|
331
308
|
|
|
332
309
|
log.info(`\nProcessing ${devicesToProcess.length} device(s)...`);
|
|
333
310
|
|
|
334
|
-
const results
|
|
311
|
+
const results = [];
|
|
335
312
|
|
|
336
313
|
for (const device of devicesToProcess) {
|
|
337
|
-
const result = await createTunnelForDevice(
|
|
314
|
+
const result = await createTunnelForDevice(
|
|
315
|
+
device,
|
|
316
|
+
tlsOptions,
|
|
317
|
+
packetStreamBaseRef,
|
|
318
|
+
);
|
|
338
319
|
results.push(result);
|
|
339
320
|
|
|
340
321
|
if (devicesToProcess.length > 1) {
|
|
@@ -353,11 +334,11 @@ async function main(): Promise<void> {
|
|
|
353
334
|
if (successful.length > 0) {
|
|
354
335
|
log.info('\n✅ Successful tunnels:');
|
|
355
336
|
const registry = await updateTunnelRegistry(results);
|
|
356
|
-
await startTunnelRegistryServer(registry);
|
|
337
|
+
await startTunnelRegistryServer(registry, registryPort);
|
|
357
338
|
|
|
358
339
|
log.info('\n📁 Tunnel registry API:');
|
|
359
340
|
log.info(' The tunnel registry is now available through the API at:');
|
|
360
|
-
log.info(
|
|
341
|
+
log.info(` http://localhost:${registryPort}/remotexpc/tunnels`);
|
|
361
342
|
log.info('\n Available endpoints:');
|
|
362
343
|
log.info(' - GET /remotexpc/tunnels - List all tunnels');
|
|
363
344
|
log.info(' - GET /remotexpc/tunnels/:udid - Get tunnel by UDID');
|
|
@@ -365,15 +346,15 @@ async function main(): Promise<void> {
|
|
|
365
346
|
|
|
366
347
|
log.info('\n💡 Example usage:');
|
|
367
348
|
log.info(
|
|
368
|
-
` curl http://localhost:${
|
|
349
|
+
` curl http://localhost:${registryPort}/remotexpc/tunnels`,
|
|
369
350
|
);
|
|
370
351
|
log.info(
|
|
371
|
-
` curl http://localhost:${
|
|
352
|
+
` curl http://localhost:${registryPort}/remotexpc/tunnels/metadata`,
|
|
372
353
|
);
|
|
373
354
|
if (successful.length > 0) {
|
|
374
355
|
const firstUdid = successful[0].device.Properties.SerialNumber;
|
|
375
356
|
log.info(
|
|
376
|
-
` curl http://localhost:${
|
|
357
|
+
` curl http://localhost:${registryPort}/remotexpc/tunnels/${firstUdid}`,
|
|
377
358
|
);
|
|
378
359
|
}
|
|
379
360
|
}
|
|
@@ -383,5 +364,4 @@ async function main(): Promise<void> {
|
|
|
383
364
|
}
|
|
384
365
|
}
|
|
385
366
|
|
|
386
|
-
|
|
387
|
-
main();
|
|
367
|
+
await main();
|
package/scripts/pair-appletv.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
import {
|
|
3
|
-
AppleTVPairingService,
|
|
4
|
-
UserInputService,
|
|
5
|
-
} from '../src/lib/apple-tv/index.js';
|
|
6
|
-
import { getLogger } from '../src/lib/logger.js';
|
|
7
|
-
|
|
8
|
-
interface CLIArgs {
|
|
9
|
-
device?: string;
|
|
10
|
-
help?: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function parseArgs(): CLIArgs {
|
|
14
|
-
const args: CLIArgs = {};
|
|
15
|
-
const cliArgs = process.argv.slice(2);
|
|
16
|
-
|
|
17
|
-
for (let i = 0; i < cliArgs.length; i++) {
|
|
18
|
-
const arg = cliArgs[i];
|
|
19
|
-
if (arg === '--device' || arg === '-d') {
|
|
20
|
-
args.device = cliArgs[++i];
|
|
21
|
-
} else if (arg === '--help' || arg === '-h') {
|
|
22
|
-
args.help = true;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return args;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function printHelp(): void {
|
|
30
|
-
// eslint-disable-next-line no-console
|
|
31
|
-
console.log(`
|
|
32
|
-
Apple TV Pairing Script
|
|
33
|
-
|
|
34
|
-
Usage: pair-appletv [options]
|
|
35
|
-
|
|
36
|
-
Options:
|
|
37
|
-
-d, --device <selector> Specify device to pair with. Can be:
|
|
38
|
-
- Device name (e.g., "Living Room")
|
|
39
|
-
- Device identifier (e.g., "AA:BB:CC:DD:EE:FF")
|
|
40
|
-
- Device index (e.g., "0", "1", "2")
|
|
41
|
-
If not specified and multiple devices are found,
|
|
42
|
-
you will be prompted to choose one.
|
|
43
|
-
-h, --help Show this help message
|
|
44
|
-
|
|
45
|
-
Examples:
|
|
46
|
-
pair-appletv # Discover and select device interactively
|
|
47
|
-
pair-appletv --device "Living Room" # Pair with device named "Living Room"
|
|
48
|
-
pair-appletv --device 0 # Pair with first discovered device
|
|
49
|
-
pair-appletv -d AA:BB:CC:DD:EE:FF # Pair with device by identifier
|
|
50
|
-
`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// CLI interface
|
|
54
|
-
async function main(): Promise<void> {
|
|
55
|
-
const log = getLogger('AppleTVPairing');
|
|
56
|
-
const args = parseArgs();
|
|
57
|
-
|
|
58
|
-
if (args.help) {
|
|
59
|
-
printHelp();
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const userInput = new UserInputService();
|
|
64
|
-
const pairingService = new AppleTVPairingService(userInput);
|
|
65
|
-
const result = await pairingService.discoverAndPair(args.device);
|
|
66
|
-
|
|
67
|
-
if (result.success) {
|
|
68
|
-
log.info(`Pairing successful! Record saved to: ${result.pairingFile}`);
|
|
69
|
-
} else {
|
|
70
|
-
const error = result.error ?? new Error('Pairing failed');
|
|
71
|
-
log.error(`Pairing failed: ${error.message}`);
|
|
72
|
-
throw error;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
main().catch(() => {
|
|
77
|
-
process.exit(1);
|
|
78
|
-
});
|