Skip to content

Commit

Permalink
feat: Introduce proper syslog scanning (#719)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Jul 22, 2024
1 parent 74fc1fa commit 0130dd0
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 259 deletions.
11 changes: 10 additions & 1 deletion driver/lib/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
} from '@appium/types';
import type { IsolateSocket } from './sessions/isolate_socket';
import type { Server } from 'node:net';
import type { LogMonitor } from './sessions/log-monitor';


type FluttertDriverConstraints = typeof desiredCapConstraints;
Expand All @@ -51,6 +52,7 @@ class FlutterDriver extends BaseDriver<FluttertDriverConstraints> {

public portForwardLocalPort: string | null;
public localServer: Server | null;
protected _logmon: LogMonitor | null;

// Used to keep the capabilities internally
public internalCaps: DriverCaps<FluttertDriverConstraints>;
Expand Down Expand Up @@ -96,6 +98,7 @@ class FlutterDriver extends BaseDriver<FluttertDriverConstraints> {
super(opts, shouldValidateCaps);
this.socket = null;
this.device = null;
this._logmon = null;
this.desiredCapConstraints = desiredCapConstraints;

// Used to keep the port for port forward to clear the pair.
Expand All @@ -106,14 +109,20 @@ class FlutterDriver extends BaseDriver<FluttertDriverConstraints> {
}

public async createSession(...args): Promise<DefaultCreateSessionResult<FluttertDriverConstraints>> {
const [sessionId, caps] = await super.createSession(...JSON.parse(JSON.stringify(args)) as [W3CDriverCaps, W3CDriverCaps, W3CDriverCaps, DriverData[]]);
const [sessionId, caps] = await super.createSession(...JSON.parse(JSON.stringify(args)) as [
W3CDriverCaps, W3CDriverCaps, W3CDriverCaps, DriverData[]
]);
this.internalCaps = caps;
return createSession.bind(this)(sessionId, caps, ...JSON.parse(JSON.stringify(args)));
}

public async deleteSession() {
this.log.info(`Deleting Flutter Driver session`);

this._logmon?.stop();
this._logmon = null;
this.proxydriver?.eventEmitter?.removeAllListeners('syslogStarted');

this.log.info('Cleanup the port forward');
switch (_.toLower(this.internalCaps.platformName)) {
case PLATFORM.IOS:
Expand Down
82 changes: 54 additions & 28 deletions driver/lib/sessions/android.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import AndroidUiautomator2Driver from 'appium-uiautomator2-driver';
import { log } from '../logger';
import { connectSocket, fetchObservatoryUrl } from './observatory';
import type { InitialOpts } from '@appium/types';
import { connectSocket, extractObservatoryUrl, OBSERVATORY_URL_PATTERN } from './observatory';
import type { InitialOpts, StringRecord } from '@appium/types';
import type { IsolateSocket } from './isolate_socket';
import { FlutterDriver } from '../driver';
import { LogMonitor } from './log-monitor';
import type { LogEntry } from './log-monitor';

const setupNewAndroidDriver = async (...args: any[]): Promise<AndroidUiautomator2Driver> => {
export async function startAndroidSession(
this: FlutterDriver,
caps: Record<string, any>,
...args: any[]
): Promise<[AndroidUiautomator2Driver, IsolateSocket|null]> {
this.log.info(`Starting an Android proxy session`);
const androiddriver = new AndroidUiautomator2Driver({} as InitialOpts);
if (!caps.observatoryWsUri) {
androiddriver.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-ignore Args are ok
await androiddriver.createSession(...args);
return androiddriver;
};

export const startAndroidSession = async (
flutterDriver: FlutterDriver,
caps: Record<string, any>,
...args: any[]
): Promise<[AndroidUiautomator2Driver, IsolateSocket|null]> => {
log.info(`Starting an Android proxy session`);
const androiddriver = await setupNewAndroidDriver(...args);

// the session starts without any apps
if (caps.app === undefined && caps.appPackage === undefined) {
Expand All @@ -27,19 +35,24 @@ export const startAndroidSession = async (

return [
androiddriver,
await connectSocket(getObservatoryWsUri, flutterDriver, androiddriver, caps),
await connectAndroidSession.bind(this)(androiddriver, caps),
];
};
}

export const connectAndroidSession = async (
flutterDriver: FlutterDriver, androiddriver: AndroidUiautomator2Driver, caps: Record<string, any>
): Promise<IsolateSocket> =>
await connectSocket(getObservatoryWsUri, flutterDriver, androiddriver, caps);
export async function connectAndroidSession (
this: FlutterDriver,
androiddriver: AndroidUiautomator2Driver,
caps: Record<string, any>
): Promise<IsolateSocket> {
const observatoryWsUri = await getObservatoryWsUri.bind(this)(androiddriver, caps);
return await connectSocket.bind(this)(observatoryWsUri, caps);
}

export const getObservatoryWsUri = async (
flutterDriver: FlutterDriver,
export async function getObservatoryWsUri (
this: FlutterDriver,
proxydriver: AndroidUiautomator2Driver,
caps): Promise<string> => {
caps: StringRecord,
): Promise<string> {
let urlObject: URL;
if (caps.observatoryWsUri) {
urlObject = new URL(caps.observatoryWsUri);
Expand All @@ -50,14 +63,27 @@ export const getObservatoryWsUri = async (
return urlObject.toJSON();
}
} else {
urlObject = fetchObservatoryUrl(proxydriver.adb.logcat!.logs as [{message: string}]);
if (!this._logmon) {
throw new Error(
`The mandatory logcat service must be running in order to initialize the Flutter driver. ` +
`Have you disabled it in capabilities?`
);
}
if (!this._logmon.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-userland/appium-flutter-driver#usage and that it does not crash on startup.`
);
}
urlObject = extractObservatoryUrl(this._logmon.lastMatch) as URL;
}
const remotePort = urlObject.port;
flutterDriver.portForwardLocalPort = caps.forwardingPort ?? remotePort;
urlObject.port = flutterDriver.portForwardLocalPort!;
await proxydriver.adb.forwardPort(flutterDriver.portForwardLocalPort!, remotePort);
this.portForwardLocalPort = caps.forwardingPort ?? remotePort;
urlObject.port = this.portForwardLocalPort as string;
await proxydriver.adb.forwardPort(this.portForwardLocalPort as string, remotePort);
if (!caps.observatoryWsUri && proxydriver.adb.adbHost) {
urlObject.host = proxydriver.adb.adbHost;
}
return urlObject.toJSON();
};
}
111 changes: 71 additions & 40 deletions driver/lib/sessions/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,38 @@ import XCUITestDriver from 'appium-xcuitest-driver';
import B from 'bluebird';
import net from 'net';
import { checkPortStatus } from 'portscanner';
import { log } from '../logger';
import { connectSocket, fetchObservatoryUrl } from './observatory';
import {
connectSocket,
extractObservatoryUrl,
OBSERVATORY_URL_PATTERN
} from './observatory';
import type { InitialOpts } from '@appium/types';
import type { IsolateSocket } from './isolate_socket';
import { FlutterDriver } from '../driver';
import { LogMonitor } from './log-monitor';
import type { LogEntry } from './log-monitor';
import type { FlutterDriver } from '../driver';

const LOCALHOST = `127.0.0.1`;

const setupNewIOSDriver = async (...args: any[]): Promise<XCUITestDriver> => {
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 InitialOpts);
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();
});
}
await iosdriver.createSession(...args);
return iosdriver;
};

export const startIOSSession = async (
flutterDriver: FlutterDriver,
caps: Record<string, any>, ...args: any[]
): Promise<[XCUITestDriver, IsolateSocket|null]> => {
log.info(`Starting an IOS proxy session`);
const iosdriver = await setupNewIOSDriver(...args);

// the session starts without any apps
if (caps.app === undefined && caps.bundleId === undefined) {
Expand All @@ -31,29 +43,35 @@ export const startIOSSession = async (

return [
iosdriver,
await connectSocket(getObservatoryWsUri, flutterDriver, iosdriver, caps),
await connectIOSSession.bind(this)(iosdriver, caps),
];
};
}

export const connectIOSSession = async (
flutterDriver: FlutterDriver,
iosdriver: XCUITestDriver, caps: Record<string, any>
): Promise<IsolateSocket> =>
await connectSocket(getObservatoryWsUri, flutterDriver, iosdriver, caps);
export async function connectIOSSession(
this: FlutterDriver,
iosdriver: XCUITestDriver,
caps: Record<string, any>
): Promise<IsolateSocket> {
const observatoryWsUri = await getObservatoryWsUri.bind(this)(iosdriver, caps);
return await connectSocket.bind(this)(observatoryWsUri, iosdriver, caps);
}

async function requireFreePort (port: number) {
async function requireFreePort(
this: FlutterDriver,
port: number
) {
if ((await checkPortStatus(port, LOCALHOST)) !== `open`) {
return;
}
log.warn(`Port #${port} is busy. Did you quit the previous driver session(s) properly?`);
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 const getObservatoryWsUri = async (
flutterDriver: FlutterDriver,
export async function getObservatoryWsUri (
this: FlutterDriver,
proxydriver: XCUITestDriver, caps: Record<string, any>
): Promise<string> => {
): Promise<string> {
let urlObject;
if (caps.observatoryWsUri) {
urlObject = new URL(caps.observatoryWsUri);
Expand All @@ -64,21 +82,34 @@ export const getObservatoryWsUri = async (
return urlObject.toJSON();
}
} else {
urlObject = fetchObservatoryUrl(proxydriver.logs.syslog.logs);
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?`
);
}
if (!this._logmon.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-userland/appium-flutter-driver#usage and that it does not crash on startup.`
);
}
urlObject = extractObservatoryUrl(this._logmon.lastMatch) as URL;
}
if (!proxydriver.isRealDevice()) {
log.info(`Running on iOS simulator`);
this.log.info(`Running on iOS simulator`);
return urlObject.toJSON();
}

const remotePort = urlObject.port;
const localPort = caps.forwardingPort ?? remotePort;
urlObject.port = localPort;

log.info(`Running on iOS real device`);
this.log.info(`Running on iOS real device`);
const { udid } = proxydriver.opts;
await requireFreePort(localPort);
flutterDriver.localServer = net.createServer(async (localSocket) => {
await requireFreePort.bind(this)(localPort);
this.localServer = net.createServer(async (localSocket) => {
let remoteSocket;
try {
remoteSocket = await utilities.connectPort(udid, remotePort);
Expand All @@ -95,34 +126,34 @@ export const getObservatoryWsUri = async (
destroyCommChannel();
localSocket.destroy();
});
remoteSocket.on('error', (e) => log.debug(e));
remoteSocket.on('error', (e) => this.log.debug(e));

localSocket.once(`end`, destroyCommChannel);
localSocket.once(`close`, () => {
destroyCommChannel();
remoteSocket.destroy();
});
localSocket.on('error', (e) => log.warn(e.message));
localSocket.on('error', (e) => this.log.warn(e.message));
localSocket.pipe(remoteSocket);
remoteSocket.pipe(localSocket);
});
const listeningPromise = new B((resolve, reject) => {
flutterDriver.localServer?.once(`listening`, resolve);
flutterDriver.localServer?.once(`error`, reject);
this.localServer?.once(`listening`, resolve);
this.localServer?.once(`error`, reject);
});
flutterDriver.localServer?.listen(localPort);
this.localServer?.listen(localPort);
try {
await listeningPromise;
} catch (e) {
flutterDriver.localServer = null;
this.localServer = null;
throw new Error(`Cannot listen on the local port ${localPort}. Original error: ${e.message}`);
}

log.info(`Forwarding the remote port ${remotePort} to the local port ${localPort}`);
this.log.info(`Forwarding the remote port ${remotePort} to the local port ${localPort}`);

process.on(`beforeExit`, () => {
flutterDriver.localServer?.close();
flutterDriver.localServer = null;
this.localServer?.close();
this.localServer = null;
});
return urlObject.toJSON();
};
}
Loading

0 comments on commit 0130dd0

Please sign in to comment.