* Copyright (c) 2025 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { execCommand, execCommandWithOsascript } from './commandExecutor';
import { TunnelInfo } from './tunnel/tunnelInfo';
import { TunnelProtocol } from './tunnel/tunnelProtocol';
export interface DebugServerConnectInfo {
address: string;
port: number;
connectCommand: string;
}
* Utility for interacting with `pymobiledevice3`
*/
export class PyMobileDevice3 {
private static readonly IOS_TUNNEL_ADMIN_PROMPT =
'Administrator privileges are required to start the iOS tunnel.';
private static readonly START_TUNNEL_COMMAND = 'pymobiledevice3 lockdown start-tunnel';
private static readonly START_DEBUG_SERVER_COMMAND = 'pymobiledevice3 developer debugserver start-server';
public static pythonPath = 'python3';
* Starts a `pymobiledevice3` tunnel
*
* @returns Tunnel information
*/
public static async startTunnel(): Promise<TunnelInfo> {
let pyCommand = `${this.pythonPath} -m ${this.START_TUNNEL_COMMAND}`;
let result = await execCommandWithOsascript(
pyCommand,
'--rsd',
this.IOS_TUNNEL_ADMIN_PROMPT);
let tunnelInfo = this.parseTunnelOutput(result.output);
tunnelInfo.pid = result.pid;
return tunnelInfo;
}
* Starts a debug server through the tunnel
*
* @param tunnelInfo Tunnel connection information
* @returns Debug server connection details
*/
public static async startDebugServer(tunnelInfo: TunnelInfo): Promise<DebugServerConnectInfo> {
let command = `${this.START_DEBUG_SERVER_COMMAND} ${tunnelInfo.rsdOption}`;
let pyCommand = `${this.pythonPath} -m ${command}`;
let result = await execCommand(pyCommand);
return this.parseDebugServerOutput(result.output);
}
* Parses debug server output to extract connection information
*
* @param output Command output text
* @returns Debug server connection details
*/
public static async parseDebugServerOutput(output: string): Promise<DebugServerConnectInfo> {
if (!output || output.trim().length === 0) {
throw new Error('Failed to start debugserver');
}
let match = output.match(/connect:\/\/\[(.+?)\]:(\d+)/);
if (!match) {
throw new Error(`Failed to parse debugserver connection info from output.\nOutput:\n${output}`);
}
let address = match[1];
let port = Number(match[2]);
if (Number.isNaN(port)) {
throw new Error(`invalid port parsed from debugserver output: ${match[0]}`);
}
return { address, port, connectCommand: `connect://[${address}]:${port}` };
}
* Parses tunnel command output
*
* @param output Command output text
* @returns Tunnel information
*/
public static parseTunnelOutput(output: string): TunnelInfo {
let info: TunnelInfo = {
pid: 0,
identifier: '',
};
let lines = output.split(/\r?\n/);
let kvHandlers: Record<string, (v: string) => void> = {
'Identifier': v => info.identifier = v,
'Interface': v => info.interface = v,
'Protocol': v => info.protocol = v.split('.')[1] === 'TCP' ? TunnelProtocol.TCP : TunnelProtocol.QUIC,
'RSD Address': v => info.rsdAddress = v,
'RSD Port': v => info.rsdPort = Number(v),
};
let expectRsdOption = false;
for (let rawLine of lines) {
let line = rawLine.trim();
if (!line) {
continue;
}
let sepIndex = line.indexOf(':');
if (sepIndex !== -1) {
let key = line.slice(0, sepIndex).trim();
let value = line.slice(sepIndex + 1).trim();
let handler = kvHandlers[key];
if (handler) {
handler(value);
continue;
}
if (key === 'Use the follow connection option') {
expectRsdOption = true;
continue;
}
}
if (expectRsdOption && line.startsWith('--rsd ')) {
info.rsdOption = line;
expectRsdOption = false;
}
if (!info.identifier ||
!info.interface ||
!info.rsdAddress ||
!info.rsdPort ||
!info.rsdOption) {
console.log('Failed to parse tunnel creation output line: ', line);
}
}
return info;
}
}