( udid: string, spawn: SpawnImpl = defaultSpawn, )
| 108 | * fails — callers should fall through to mDNS resolution. |
| 109 | */ |
| 110 | export function getDeviceTunnelIPv6FromDevicectl( |
| 111 | udid: string, |
| 112 | spawn: SpawnImpl = defaultSpawn, |
| 113 | ): string | null { |
| 114 | const tmp = join(tmpdir(), `devicectl-details-${process.pid}-${Date.now()}.json`); |
| 115 | try { |
| 116 | const r = spawn('xcrun', ['devicectl', 'device', 'info', 'details', '--device', udid, '--json-output', tmp]); |
| 117 | if (r.status !== 0) return null; |
| 118 | const raw = readFileSync(tmp, 'utf-8'); |
| 119 | const obj = JSON.parse(raw); |
| 120 | // `result.connectionProperties.tunnelIPAddress` is the canonical location. |
| 121 | // Some Xcode/CoreDevice versions also surface it under `result.tunnel.ipAddress` |
| 122 | // — accept either. |
| 123 | const conn = obj?.result?.connectionProperties as Record<string, unknown> | undefined; |
| 124 | const tunnel = obj?.result?.tunnel as Record<string, unknown> | undefined; |
| 125 | const addr = (conn?.tunnelIPAddress ?? tunnel?.ipAddress) as string | undefined; |
| 126 | if (typeof addr === 'string' && addr.includes(':')) return addr; |
| 127 | return null; |
| 128 | } catch { |
| 129 | return null; |
| 130 | } finally { |
| 131 | try { rmSync(tmp, { force: true }); } catch { /* ignore */ } |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | /** |
| 136 | * Start a periodic devicectl `info details` poll that keeps the CoreDevice |
no test coverage detected