appium-remote-debugger
Version:
Appium proxy for Remote Debugger protocol
206 lines (185 loc) • 6.53 kB
text/typescript
import {log} from '../logger';
import _ from 'lodash';
import B from 'bluebird';
import net from 'node:net';
import {RpcClient} from './rpc-client';
import {services} from 'appium-ios-device';
import type {RpcClientOptions, RpcClientSimulatorOptions, RemoteCommand} from '../types';
/**
* RPC client implementation for iOS simulators.
* Extends RpcClient to provide simulator-specific connection handling
* via TCP sockets or Unix domain sockets.
*/
export class RpcClientSimulator extends RpcClient {
protected readonly host?: string;
protected port?: number;
protected readonly messageProxy?: any;
protected socket: net.Socket | null;
protected readonly socketPath?: string;
protected service?: any;
/**
* @param opts - Options for configuring the RPC client, including
* simulator-specific options like socketPath, host, and port.
*/
constructor(opts: RpcClientOptions & RpcClientSimulatorOptions = {}) {
super(opts);
const {socketPath, host = '::1', port, messageProxy} = opts;
// host/port config for TCP communication, socketPath for unix domain sockets
this.host = host;
this.port = port;
this.messageProxy = messageProxy;
this.socket = null;
this.socketPath = socketPath;
}
/**
* Connects to the Web Inspector service on an iOS simulator.
* Supports both Unix domain sockets and TCP connections, with optional proxy support.
*/
override async connect(): Promise<void> {
// create socket and handle its messages
if (this.socketPath) {
if (this.messageProxy) {
// unix domain socket via proxy
log.debug(
`Connecting to remote debugger via proxy through unix domain socket: '${this.messageProxy}'`,
);
this.socket = net.connect(this.messageProxy);
// Forward the actual socketPath to the proxy
this.socket.once('connect', () => {
log.debug(
`Forwarding the actual web inspector socket to the proxy: '${this.socketPath}'`,
);
if (this.socket) {
this.socket.write(
JSON.stringify({
socketPath: this.socketPath,
}),
);
}
});
} else {
// unix domain socket
log.debug(`Connecting to remote debugger through unix domain socket: '${this.socketPath}'`);
this.socket = net.connect(this.socketPath);
}
} else {
if (this.messageProxy) {
// connect to the proxy instead of the remote debugger directly
this.port = this.messageProxy;
}
// tcp socket
log.debug(
`Connecting to remote debugger ${this.messageProxy ? 'via proxy ' : ''}through TCP: ${this.host}:${this.port}`,
);
this.socket = new net.Socket();
if (this.port && this.host) {
this.socket.connect(this.port, this.host);
} else {
throw new Error('Both port and host must be defined for TCP connection');
}
}
this.socket.setNoDelay(true);
this.socket.setKeepAlive(true);
this.socket.on('close', () => {
if (this.isConnected) {
log.debug('Debugger socket disconnected');
}
this.isConnected = false;
this.socket = null;
});
this.socket.on('end', () => {
this.isConnected = false;
});
this.service = await services.startWebInspectorService(this.udid, {
socket: this.socket,
isSimulator: true,
osVersion: this.platformVersion,
verbose: this.logAllCommunication,
verboseHexDump: this.logAllCommunicationHexDump,
maxFrameLength: this.webInspectorMaxFrameLength,
});
this.service.listenMessage(this.receive.bind(this));
// connect the socket
return await new B<void>((resolve, reject) => {
// only resolve this function when we are actually connected
if (!this.socket) {
return reject(new Error('RPC socket is not connected. Please contact developers'));
}
this.socket.on('connect', () => {
log.debug(`Debugger socket connected`);
this.isConnected = true;
resolve();
});
this.socket.on('error', (err) => {
if (this.isConnected) {
log.error(`Socket error: ${err.message}`);
this.isConnected = false;
}
// the connection was refused, so reject the connect promise
reject(err);
});
});
}
/**
* Disconnects from the Web Inspector service on the simulator.
* Closes the socket and service connection, and cleans up resources.
*/
override async disconnect(): Promise<void> {
if (!this.isConnected) {
return;
}
log.debug('Disconnecting from remote debugger');
await super.disconnect();
this.service?.close();
this.isConnected = false;
}
/**
* Sends a command message to the Web Inspector service via the socket.
* Handles socket errors and ensures the socket is available before sending.
*
* @param cmd - The command to send to the simulator.
*/
override async sendMessage(cmd: RemoteCommand): Promise<void> {
let onSocketError: ((err: Error) => void) | undefined;
return await new B<void>((resolve, reject) => {
// handle socket problems
onSocketError = (err: Error) => {
log.error(`Socket error: ${err.message}`);
// the connection was refused, so reject the connect promise
reject(err);
};
if (!this.socket || !this.service) {
return reject(
new Error(
'The RPC client is not connected. Have you called `connect()` before sending a message?',
),
);
}
this.socket.on('error', onSocketError);
this.service.sendMessage(cmd);
resolve();
}).finally(() => {
// remove this listener, so we don't exhaust the system
if (this.socket && onSocketError) {
this.socket.removeListener('error', onSocketError);
}
});
}
/**
* Receives data from the Web Inspector service and handles it.
* Converts Buffer data to strings for certain message keys.
*
* @param data - The data received from the service.
*/
override async receive(data: any): Promise<void> {
if (!this.isConnected || !data) {
return;
}
for (const key of ['WIRMessageDataKey', 'WIRDestinationKey', 'WIRSocketDataKey']) {
if (!_.isUndefined(data[key])) {
data[key] = data[key].toString('utf8');
}
}
await this.messageHandler.handleMessage(data);
}
}