appium-flutter-driver
Version:
Appium Flutter driver
187 lines (170 loc) • 6.09 kB
text/typescript
import {utilities} from 'appium-ios-device';
import {XCUITestDriver} from 'appium-xcuitest-driver';
import B from 'bluebird';
import net from 'node:net';
import {checkPortStatus} from 'portscanner';
import {connectSocket, extractObservatoryUrl, OBSERVATORY_URL_PATTERN} from './observatory';
import type {IsolateSocket} from './isolate_socket';
import {LogMonitor} from './log-monitor';
import type {LogEntry} from './log-monitor';
import type {FlutterDriver} from '../driver';
import type {XCUITestDriverOpts} from 'appium-xcuitest-driver/build/lib/driver';
const LOCALHOST = `127.0.0.1`;
export async function startIOSSession(
this: FlutterDriver,
caps: Record<string, any>,
...args: any[]
): Promise<[XCUITestDriver, IsolateSocket | null]> {
this.log.info(`Starting an IOS proxy session`);
const iosdriver = new XCUITestDriver({} as XCUITestDriverOpts);
if (!caps.observatoryWsUri) {
iosdriver.eventEmitter.once('syslogStarted', (syslog) => {
this._logmon = new LogMonitor(syslog, async (entry: LogEntry) => {
if (extractObservatoryUrl(entry)) {
this.log.debug(`Matched the syslog line '${entry.message}'`);
return true;
}
return false;
});
this._logmon.start();
});
}
// @ts-expect-error can be ignored
await iosdriver.createSession(...args);
// the session starts without any apps
if (caps.app === undefined && caps.bundleId === undefined) {
return [iosdriver, null];
}
return [iosdriver, await connectIOSSession.bind(this)(iosdriver, caps)];
}
export async function connectIOSSession(
this: FlutterDriver,
iosdriver: XCUITestDriver,
caps: Record<string, any>,
clearLog: boolean = false,
): Promise<IsolateSocket> {
const observatoryWsUri = await getObservatoryWsUri.bind(this)(iosdriver, caps, clearLog);
return await connectSocket.bind(this)(observatoryWsUri, iosdriver, caps);
}
async function requireFreePort(this: FlutterDriver, port: number) {
// Try to close existing local server if it exists
if (this.localServer) {
this.log.info(`Closing existing local server on port ${port}`);
await new Promise<void>((resolve) => {
this.localServer?.close((err) => {
if (err) {
this.log.error(`Error occurred while closing the local server: ${err.message}`);
return resolve(); // Resolve even if there's an error to avoid hanging
}
this.log.info(`Previous local server closed`);
resolve();
});
});
}
if ((await checkPortStatus(port, LOCALHOST)) !== `open`) {
return;
}
this.log.warn(`Port #${port} is busy. Did you quit the previous driver session(s) properly?`);
throw new Error(
`The port :${port} is occupied by an other process. ` +
`You can either quit that process or select another free port.`,
);
}
export async function getObservatoryWsUri(
this: FlutterDriver,
proxydriver: XCUITestDriver,
caps: Record<string, any>,
clearLog: boolean = false,
): Promise<string> {
if (clearLog) {
this._logmon?.clearlastMatch();
this._logmon?.stop();
this._logmon?.start();
}
let urlObject;
if (caps.observatoryWsUri) {
urlObject = new URL(caps.observatoryWsUri);
urlObject.protocol = `ws`;
// defaults to skip the port-forwarding as backward compatibility
if (caps.skipPortForward === undefined || caps.skipPortForward) {
return urlObject.toJSON();
}
} else {
if (!this._logmon) {
throw new Error(
`The mandatory syslog service must be running in order to initialize the Flutter driver. ` +
`Have you disabled it in capabilities?`,
);
}
let lastMatch: LogEntry | null = null;
try {
lastMatch = await this._logmon.waitForLastMatchExist(
caps.maxRetryCount,
caps.retryBackoffTime,
);
} catch (e) {
this.log.error(e);
}
if (!lastMatch) {
throw new Error(
`No observatory URL matching to '${OBSERVATORY_URL_PATTERN}' was found in the device log. ` +
`Please make sure the application under test is configured properly according to ` +
`https://github.com/appium/appium-flutter-driver#usage and that it does not crash on startup.`,
);
}
urlObject = extractObservatoryUrl(lastMatch) as URL;
}
if (!proxydriver.isRealDevice()) {
this.log.info(`Running on iOS simulator`);
return urlObject.toJSON();
}
const remotePort = urlObject.port;
const localPort = caps.forwardingPort ?? remotePort;
urlObject.port = localPort;
this.log.info(`Running on iOS real device`);
const {udid} = proxydriver.opts;
await requireFreePort.bind(this)(localPort);
this.localServer = net.createServer(async (localSocket) => {
let remoteSocket;
try {
remoteSocket = await utilities.connectPort(udid, remotePort);
} catch {
localSocket.destroy();
return;
}
const destroyCommChannel = () => {
remoteSocket.unpipe(localSocket);
localSocket.unpipe(remoteSocket);
};
remoteSocket.once(`close`, () => {
destroyCommChannel();
localSocket.destroy();
});
remoteSocket.on('error', (e) => this.log.debug(e));
localSocket.once(`end`, destroyCommChannel);
localSocket.once(`close`, () => {
destroyCommChannel();
remoteSocket.destroy();
});
localSocket.on('error', (e) => this.log.warn(e.message));
localSocket.pipe(remoteSocket);
remoteSocket.pipe(localSocket);
});
const listeningPromise = new B((resolve, reject) => {
this.localServer?.once(`listening`, resolve);
this.localServer?.once(`error`, reject);
});
this.localServer?.listen(localPort);
try {
await listeningPromise;
} catch (e) {
this.localServer = null;
throw new Error(`Cannot listen on the local port ${localPort}. Original error: ${e.message}`);
}
this.log.info(`Forwarding the remote port ${remotePort} to the local port ${localPort}`);
process.on(`beforeExit`, () => {
this.localServer?.close();
this.localServer = null;
});
return urlObject.toJSON();
}