appium-ios-remotexpc 0.37.1 → 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 +6 -0
- package/README.md +10 -1
- 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,9 @@
|
|
|
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
|
+
|
|
1
7
|
## [0.37.1](https://github.com/appium/appium-ios-remotexpc/compare/v0.37.0...v0.37.1) (2026-03-21)
|
|
2
8
|
|
|
3
9
|
## [0.37.0](https://github.com/appium/appium-ios-remotexpc/compare/v0.36.0...v0.37.0) (2026-03-21)
|
package/README.md
CHANGED
|
@@ -196,10 +196,19 @@ All pull requests must pass these checks before merging. The workflows are defin
|
|
|
196
196
|
- `npm run format` - Run prettier
|
|
197
197
|
- `npm run lint:fix` - Run ESLint with auto-fix
|
|
198
198
|
- `npm test` - Run tests (requires sudo privileges for tunneling)
|
|
199
|
-
|
|
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`).
|
|
200
208
|
|
|
201
209
|
## Project Structure
|
|
202
210
|
|
|
211
|
+
- `/scripts` - Optional CLI helpers (ESM `.mjs`) for tunnels and Apple TV pairing; use via `npm run` entries under [Scripts](#scripts)
|
|
203
212
|
- `/src` - Source code
|
|
204
213
|
- `/lib` - Core libraries
|
|
205
214
|
- `/lockdown` - Device lockdown protocol
|
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
|
-
});
|