Skip to content

Commit 9641cdc

Browse files
Improve port discovery of flutter server from device to support multiple flutter apps (#5)
* Refactor device port discovery for multiple flutter apps * bump up version to beta.10
1 parent ab83d60 commit 9641cdc

File tree

7 files changed

+125
-62
lines changed

7 files changed

+125
-62
lines changed

android.conf.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ export const config: WebdriverIO.Config = {
99
platformName: 'Android',
1010
'appium:automationName': 'FlutterIntegration',
1111
'appium:orientation': 'PORTRAIT',
12-
'appium:app': process.env.APP_PATH || join(process.cwd(), 'app-debug.apk'),
12+
'appium:app':
13+
process.env.APP_PATH || join(process.cwd(), 'app-debug.apk'),
1314
'appium:newCommandTimeout': 240,
14-
'appium:skipLogcatCapture': true,
1515
},
1616
],
1717
};

package-lock.json

Lines changed: 2 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"appium",
66
"flutter"
77
],
8-
"version": "1.0.0-beta.9",
8+
"version": "1.0.0-beta.10",
99
"author": "",
1010
"license": "MIT License",
1111
"repository": {
@@ -76,15 +76,13 @@
7676
"@wdio/utils": "^8.38.0",
7777
"appium-adb": "^12.3.2",
7878
"appium-android-driver": "^9.6.2",
79-
"appium-flutter-finder": "^0.2.0",
8079
"appium-ios-device": "^2.7.16",
8180
"appium-xcuitest-driver": "^7.16.2",
8281
"async-retry": "^1.3.3",
8382
"asyncbox": "^3.0.0",
8483
"axios": "^1.7.2",
8584
"bluebird": "^3.7.2",
8685
"chai": "^5.1.1",
87-
"get-port": "^7.1.0",
8886
"lodash": "^4.17.21",
8987
"portscanner": "^2.2.0"
9088
}

src/android.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { log } from './logger';
33
import type { InitialOpts } from '@appium/types';
44
import { AppiumFlutterDriver } from './driver';
55
import ADB from 'appium-adb';
6+
import { sleep } from 'asyncbox';
7+
import { fetchFlutterServerPort, getFreePort } from './utils';
8+
69
const setupNewAndroidDriver = async (
710
...args: any[]
811
): Promise<AndroidUiautomator2Driver> => {
@@ -19,18 +22,31 @@ export const startAndroidSession = async (
1922
): Promise<[AndroidUiautomator2Driver, null]> => {
2023
log.info(`Starting an Android proxy session`);
2124
const androiddriver = await setupNewAndroidDriver(...args);
22-
23-
// the session starts without any apps
24-
caps.flutterPort = caps.flutterPort || 8600;
25-
log.info('Android session started', androiddriver);
26-
await portForward(caps.flutterPort, caps.udid);
27-
return [androiddriver, caps.flutterPort];
25+
log.info('Looking for the port in where Flutter server is listening too...');
26+
await sleep(2000);
27+
const flutterServerPort = fetchFlutterServerPort(
28+
(androiddriver.adb.logcat?.logs as [{ message: string }]) || [],
29+
);
30+
caps.flutterServerPort = await portForward(
31+
caps.udid,
32+
flutterServerPort,
33+
caps.flutterServerPort,
34+
);
35+
return [androiddriver, caps.flutterServerPort];
2836
};
2937

30-
const portForward = async (port: number, udid: string) => {
38+
const portForward = async (
39+
udid: string,
40+
devicePort: number,
41+
systemPort?: number,
42+
) => {
43+
if (!systemPort) {
44+
systemPort = await getFreePort();
45+
}
3146
let adb = new ADB();
3247
if (udid) adb.setDeviceId(udid);
33-
await adb.forwardPort(port, 8888);
48+
await adb.forwardPort(systemPort!, devicePort);
3449
const adbForwardList = await adb.getForwardList();
3550
log.info(`Port forwarding: ${JSON.stringify(adbForwardList)}`);
51+
return systemPort;
3652
};

src/driver.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import {
2323
ELEMENT_CACHE,
2424
} from './commands/element';
2525

26-
import { isFlutterDriverCommand } from './utils';
26+
import { isFlutterDriverCommand } from './utils';
2727
import { W3C_WEB_ELEMENT_IDENTIFIER } from '@appium/support/build/lib/util';
28-
import { waitForCondition } from 'asyncbox';
28+
import { sleep, waitForCondition } from 'asyncbox';
2929
import { log } from './logger';
3030

3131
const DEFAULT_FLUTTER_SERVER_PORT = 8888;
@@ -177,7 +177,7 @@ export class AppiumFlutterDriver extends BaseDriver<FlutterDriverConstraints> {
177177
}
178178

179179
async executeCommand(command: any, ...args: any) {
180-
if(isFlutterDriverCommand(command)) {
180+
if (isFlutterDriverCommand(command)) {
181181
return await super.executeCommand(command, ...args);
182182
}
183183
return await this.proxydriver.executeCommand(command as string, ...args);
@@ -202,16 +202,8 @@ export class AppiumFlutterDriver extends BaseDriver<FlutterDriverConstraints> {
202202
...JSON.parse(JSON.stringify(args)),
203203
);
204204

205-
if (
206-
this.proxydriver instanceof XCUITestDriver &&
207-
!this.proxydriver.isRealDevice()
208-
) {
209-
// @ts-ignore
210-
this.flutterPort = DEFAULT_FLUTTER_SERVER_PORT;
211-
}
212-
213205
// HACK for eliminatin socket hang up by waiting 1 sec
214-
await new Promise((r) => setTimeout(r, 1000));
206+
await sleep(1000);
215207
// @ts-ignore
216208
console.log('PageSource', await this.proxydriver.getPageSource());
217209
// @ts-ignore

src/iOS.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,32 @@ import XCUITestDriver from 'appium-xcuitest-driver';
44
import type { InitialOpts } from '@appium/types';
55
import { log } from './logger';
66
import { DEVICE_CONNECTIONS_FACTORY } from './iProxy';
7+
import { sleep } from 'asyncbox';
8+
import { fetchFlutterServerPort, getFreePort } from './utils';
79

810
const setupNewIOSDriver = async (...args: any[]): Promise<XCUITestDriver> => {
911
const iosdriver = new XCUITestDriver({} as InitialOpts);
1012
await iosdriver.createSession(...args);
1113
return iosdriver;
1214
};
1315

14-
async function portForward(iOSDriver: any, flutterPort: any, udid: any) {
15-
// Need to do this for real device
16-
if (
17-
iOSDriver instanceof XCUITestDriver &&
18-
iOSDriver.isRealDevice()
19-
) {
20-
log.info(`Forwarding port ${flutterPort} to device ${udid}`);
21-
await DEVICE_CONNECTIONS_FACTORY.requestConnection(udid, flutterPort, {
22-
usePortForwarding: true,
23-
devicePort: '8888',
24-
});
16+
const portForward = async (
17+
udid: string,
18+
systemPort: number,
19+
devicePort: any,
20+
) => {
21+
if (!systemPort) {
22+
systemPort = await getFreePort();
2523
}
26-
}
24+
log.info(
25+
`Forwarding port ${systemPort} to device port ${devicePort} ${udid}`,
26+
);
27+
await DEVICE_CONNECTIONS_FACTORY.requestConnection(udid, systemPort, {
28+
usePortForwarding: true,
29+
devicePort: devicePort,
30+
});
31+
return systemPort;
32+
};
2733

2834
export const startIOSSession = async (
2935
flutterDriver: AppiumFlutterDriver,
@@ -32,10 +38,19 @@ export const startIOSSession = async (
3238
): Promise<[XCUITestDriver, number]> => {
3339
log.info(`Starting an IOS proxy session`);
3440
const iOSDriver = await setupNewIOSDriver(...args);
35-
36-
// the session starts without any apps
37-
caps.flutterPort = caps.flutterPort || 8600;
41+
log.info('Looking for port Flutter server is listening too...');
42+
await sleep(2000);
43+
const flutterServerPort = fetchFlutterServerPort(iOSDriver.logs.syslog.logs);
44+
if (iOSDriver.isRealDevice()) {
45+
caps.flutterServerPort = await portForward(
46+
iOSDriver.udid,
47+
caps.flutterServerPort,
48+
flutterServerPort,
49+
);
50+
} else {
51+
//incase of emulator use the same port where the flutter server is running
52+
caps.flutterServerPort = flutterServerPort;
53+
}
3854
log.info('iOS session started', iOSDriver);
39-
await portForward(iOSDriver, caps.flutterPort, caps.udid);
40-
return [iOSDriver, caps.flutterPort];
55+
return [iOSDriver, caps.flutterServerPort];
4156
};

src/utils.ts

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import AndroidUiautomator2Driver from 'appium-uiautomator2-driver';
22
import XCUITestDriver from 'appium-xcuitest-driver/build/lib/driver';
3+
import { log } from './logger';
4+
import { findAPortNotInUse } from 'portscanner';
5+
6+
const FLUTTER_SERVER_START_MESSAGE = new RegExp(
7+
/Appium flutter server is listening on port (\d+)/,
8+
);
9+
const DEVICE_PORT_RANGE = [9000, 9299];
310

411
export async function getProxyDriver(
512
strategy: string,
@@ -17,15 +24,57 @@ export async function getProxyDriver(
1724
}
1825
}
1926

20-
2127
export function isFlutterDriverCommand(command: string) {
22-
return [
23-
"createSession", "deleteSession", "getSession",
24-
"getSessions", "findElement", "findElements",
25-
"findElementFromElement", "findElementsFromElement", "click",
26-
"getText", "setValue", "keys", "getName", "clear",
27-
"elementSelected", "elementEnabled",
28-
"getAttribute", "elementDisplayed", "execute"
29-
]
30-
.indexOf(command) >= 0;
31-
}
28+
return (
29+
[
30+
'createSession',
31+
'deleteSession',
32+
'getSession',
33+
'getSessions',
34+
'findElement',
35+
'findElements',
36+
'findElementFromElement',
37+
'findElementsFromElement',
38+
'click',
39+
'getText',
40+
'setValue',
41+
'keys',
42+
'getName',
43+
'clear',
44+
'elementSelected',
45+
'elementEnabled',
46+
'getAttribute',
47+
'elementDisplayed',
48+
'execute',
49+
].indexOf(command) >= 0
50+
);
51+
}
52+
53+
export function fetchFlutterServerPort(
54+
deviceLogs: [{ message: string }],
55+
): number {
56+
let port: number | undefined;
57+
for (const line of deviceLogs.map((e) => e.message).reverse()) {
58+
const match = line.match(FLUTTER_SERVER_START_MESSAGE);
59+
if (match) {
60+
log.info(`Found the server started log from device: ${line}`);
61+
port = Number(match[1]);
62+
break;
63+
}
64+
}
65+
if (!port) {
66+
throw new Error(
67+
`Flutter server started message '${FLUTTER_SERVER_START_MESSAGE}' was not found in the device log. ` +
68+
`Please make sure the application under test is configured properly according to ` +
69+
`https://github.com/AppiumTestDistribution/appium-flutter-server and that it does not crash on startup.
70+
Also make sure "appium:skipLogcatCapture" is not set to true in the desired capabilities. `,
71+
);
72+
}
73+
log.info(`Extracted the port from the device logs: ${port}`);
74+
return port;
75+
}
76+
77+
export async function getFreePort() {
78+
const [start, end] = DEVICE_PORT_RANGE;
79+
return await findAPortNotInUse(start, end);
80+
}

0 commit comments

Comments
 (0)