webdriverio-automation
Version:
WebdriverIO-Automation android ios project
382 lines (325 loc) • 13 kB
JavaScript
import log from './logger';
import _ from 'lodash';
import bplistCreate from 'bplist-creator';
import bplistParser from 'bplist-parser';
import bufferpack from 'bufferpack';
import Promise from 'bluebird';
import { REMOTE_DEBUGGER_PORT } from './remote-debugger';
import UUID from 'uuid-js';
import net from 'net';
import RpcMessageHandler from './remote-debugger-message-handler';
import getRemoteCommand from './remote-messages';
export default class RemoteDebuggerRpcClient {
constructor (opts = {}) {
const {
host = '::1',
port = REMOTE_DEBUGGER_PORT,
socketPath,
specialMessageHandlers = {},
messageProxy,
} = opts;
// host/port config for TCP communication, socketPath for unix domain sockets
this.host = host;
this.port = port;
this.socketPath = socketPath;
this.messageProxy = messageProxy;
this.socket = null;
this.connected = false;
this.connId = UUID.create().toString();
this.senderId = UUID.create().toString();
this.curMsgId = 0;
this.received = Buffer.alloc(0);
this.readPos = 0;
// message handlers
this.specialMessageHandlers = specialMessageHandlers;
this.messageHandler = null;
}
async connect () {
this.messageHandler = new RpcMessageHandler(this.specialMessageHandlers);
// 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}'`);
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({type: 'tcp6'});
this.socket.connect(this.port, this.host);
}
this.socket.setNoDelay(true);
this.socket.on('close', () => {
if (this.connected) {
log.debug('Debugger socket disconnected');
}
this.connected = false;
this.socket = null;
});
this.socket.on('end', () => {
this.connected = false;
});
this.socket.on('data', this.receive.bind(this));
// connect the socket
return await new Promise((resolve, reject) => {
// only resolve this function when we are actually connected
this.socket.on('connect', () => {
log.debug(`Debugger socket connected`);
this.connected = true;
resolve();
});
this.socket.on('error', (err) => {
if (this.connected) {
log.error(`Socket error: ${err.message}`);
this.connected = false;
}
// the connection was refused, so reject the connect promise
reject(err);
});
});
}
async disconnect () { // eslint-disable-line require-await
if (this.isConnected()) {
log.debug('Disconnecting from remote debugger');
this.socket.destroy();
}
this.connected = false;
}
isConnected () {
return this.connected;
}
setSpecialMessageHandler (key, errorHandler, handler) {
this.messageHandler.setSpecialMessageHandler(key, errorHandler, handler);
}
getSpecialMessageHandler (key) {
return this.messageHandler.getSpecialMessageHandler(key);
}
setDataMessageHandler (key, errorHandler, handler) {
this.messageHandler.setDataMessageHandler(key, errorHandler, handler);
}
allowNavigationWithoutReload (allow = true) {
this.messageHandler.allowNavigationWithoutReload(allow);
}
async selectApp (appIdKey, applicationConnectedHandler) {
return await new Promise((resolve, reject) => {
// local callback, temporarily added as callback to
// `_rpc_applicationConnected:` remote debugger response
// to handle the initial connection
let onAppChange = (dict) => {
// from the dictionary returned, get the ids
let oldAppIdKey = dict.WIRHostApplicationIdentifierKey;
let correctAppIdKey = dict.WIRApplicationIdentifierKey;
// if this is a report of a proxy redirect from the remote debugger
// we want to update our dictionary and get a new app id
if (oldAppIdKey && correctAppIdKey !== oldAppIdKey) {
log.debug(`We were notified we might have connected to the wrong app. ` +
`Using id ${correctAppIdKey} instead of ${oldAppIdKey}`);
}
applicationConnectedHandler(dict);
reject(new Error('New application has connected'));
};
this.setSpecialMessageHandler('_rpc_applicationConnected:', reject, onAppChange);
// do the actual connecting to the app
return (async () => {
let [connectedAppIdKey, pageDict] = await this.send('connectToApp', {
appIdKey
});
// sometimes the connect logic happens, but with an empty dictionary
// which leads to the remote debugger getting disconnected, and into a loop
if (_.isEmpty(pageDict)) {
let msg = 'Empty page dictionary received';
log.debug(msg);
reject(new Error(msg));
} else {
resolve([connectedAppIdKey, pageDict]);
}
})();
}).finally(() => {
// no matter what, we want to restore the handler that was changed.
this.setSpecialMessageHandler('_rpc_applicationConnected:', null, applicationConnectedHandler);
});
}
async send (command, opts = {}) { // eslint-disable-line require-await
// error listener, which needs to be removed after the promise is resolved
let onSocketError;
return new Promise((resolve, reject) => {
// promise to be resolved whenever remote debugger
// replies to our request
// retrieve the correct command to send
opts = _.defaults({connId: this.connId, senderId: this.senderId}, opts);
let data = getRemoteCommand(command, opts);
// most of the time we don't care when socket.write does
// so give it an empty function
let socketCb = _.noop;
// handle socket problems
onSocketError = (exception) => {
if (this.connected) {
log.error(`Socket error: ${exception.message}`);
}
// the connection was refused, so reject the connect promise
reject(exception);
};
this.socket.on('error', onSocketError);
if (this.messageHandler.hasSpecialMessageHandler(data.__selector)) {
// special replies will return any number of arguments
// temporarily wrap with promise handling
let specialMessageHandler = this.getSpecialMessageHandler(data.__selector);
this.setSpecialMessageHandler(data.__selector, reject, function (...args) {
log.debug(`Received response from socket send: '${_.truncate(JSON.stringify(args), {length: 50})}'`);
// call the original listener, and put it back, if necessary
specialMessageHandler(...args);
if (this.messageHandler.hasSpecialMessageHandler(data.__selector)) {
// this means that the system has not removed this listener
this.setSpecialMessageHandler(data.__selector, null, specialMessageHandler);
}
resolve(args);
}.bind(this));
} else if (data.__argument && data.__argument.WIRSocketDataKey) {
// keep track of the messages coming and going using
// a simple sequential id
this.curMsgId++;
const errorHandler = function (err) {
const msg = `Remote debugger error with code '${err.code}': ${err.message}`;
reject(new Error(msg));
};
this.setDataMessageHandler(this.curMsgId.toString(), errorHandler, (value) => {
const msg = _.truncate(_.isString(value) ? value : JSON.stringify(value), {length: 50});
log.debug(`Received data response from socket send: '${msg}'`);
log.debug(`Original command: ${command}`);
resolve(value);
});
data.__argument.WIRSocketDataKey.id = this.curMsgId;
data.__argument.WIRSocketDataKey =
Buffer.from(JSON.stringify(data.__argument.WIRSocketDataKey));
} else {
// we want to immediately resolve this socket.write
// any long term callbacks will do their business in the background
socketCb = resolve;
}
log.debug(`Sending '${data.__selector}' message to remote debugger`);
// remote debugger expects a binary plist as data
let plist;
try {
plist = bplistCreate(data);
} catch (e) {
let msg = `Could not create binary plist from data: ${e.message}`;
log.error(msg);
return reject(new Error(msg));
}
if (this.socket && this.connected) {
// cork and uncork in order to not buffer the write
// on some systems this is necessary or the server
// gets confused.
this.socket.cork();
try {
this.socket.write(bufferpack.pack('L', [plist.length]));
this.socket.write(plist, socketCb);
} finally {
this.socket.uncork();
}
} else {
let msg = 'Attempted to write data to socket after it was closed!';
log.error(msg);
reject(new Error(msg));
}
})
.finally(() => {
// remove this listener, so we don't exhaust the system
this.socket.removeListener('error', onSocketError);
});
}
receive (data) {
// Append this new data to the existing Buffer
this.received = Buffer.concat([this.received, data]);
let dataLeftOver = true;
// Parse multiple messages in the same packet
while (dataLeftOver) {
// Store a reference to where we were
let oldReadPos = this.readPos;
// Read the prefix (plist length) to see how far to read next
// It's always 4 bytes long
let prefix = this.received.slice(this.readPos, this.readPos + 4);
let msgLength;
try {
msgLength = bufferpack.unpack('L', prefix)[0];
} catch (e) {
log.error(`Buffer could not unpack: ${e}`);
return;
}
// Jump forward 4 bytes
this.readPos += 4;
// Is there enough data here?
// If not, jump back to our original position and gtfo
if (this.received.length < msgLength + this.readPos) {
this.readPos = oldReadPos;
break;
}
// Extract the main body of the message (where the plist should be)
let body = this.received.slice(this.readPos, msgLength + this.readPos);
// Extract the plist
let plist;
try {
plist = bplistParser.parseBuffer(body);
} catch (e) {
log.error(`Error parsing binary plist: ${e}`);
return;
}
// bplistParser.parseBuffer returns an array
if (plist.length === 1) {
plist = plist[0];
}
for (let key of ['WIRMessageDataKey', 'WIRDestinationKey', 'WIRSocketDataKey']) {
if (!_.isUndefined(plist[key])) {
plist[key] = plist[key].toString("utf8");
}
}
// Jump forward the length of the plist
this.readPos += msgLength;
// Calculate how much buffer is left
let leftOver = this.received.length - this.readPos;
// Is there some left over?
if (leftOver !== 0) {
// Copy what's left over into a new buffer, and save it for next time
let chunk = Buffer.alloc(leftOver);
this.received.copy(chunk, 0, this.readPos);
this.received = chunk;
} else {
// Otherwise, empty the buffer and get out of the loop
this.received = Buffer.alloc(0);
dataLeftOver = false;
}
// Reset the read position
this.readPos = 0;
// Now do something with the plist
if (plist) {
this.messageHandler.handleMessage(plist);
}
}
}
setTimelineEventHandler (timelineEventHandler) {
this.timelineEventHandler = timelineEventHandler;
this.messageHandler.setTimelineEventHandler(timelineEventHandler);
}
setConsoleLogEventHandler (consoleEventHandler) {
this.consoleEventHandler = consoleEventHandler;
this.messageHandler.setConsoleLogEventHandler(consoleEventHandler);
}
setNetworkLogEventHandler (networkEventHandler) {
this.networkEventHandler = networkEventHandler;
this.messageHandler.setNetworkEventHandler(networkEventHandler);
}
}