Skip to content

Commit

Permalink
feat(android-setup): choose host port dynamically (#2933)
Browse files Browse the repository at this point in the history
This PR uses the portfinder NPM package to have our service configurator dynamically choose a free host port to bind to, rather than unilaterally choosing the same port hardcoded into the service.
  • Loading branch information
dbjorge authored Jun 23, 2020
1 parent de5ef6a commit 23db22f
Show file tree
Hide file tree
Showing 15 changed files with 388 additions and 140 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
"lodash": "^4.17.15",
"moment": "^2.27.0",
"office-ui-fabric-react": "7.98.0",
"portfinder": "^1.0.26",
"q": "1.5.1",
"react": "^16.13.1",
"react-copy-to-clipboard": "^5.0.2",
Expand Down
1 change: 1 addition & 0 deletions src/electron/flux/store/android-setup-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export class AndroidSetupStore extends BaseStoreImpl<AndroidSetupStoreData> {
stepTransition: this.stepTransition,
setSelectedDevice: this.setSelectedDevice,
setAvailableDevices: this.setAvailableDevices,
getScanPort: () => this.state.scanPort,
setScanPort: this.setScanPort,
setApplicationName: this.setApplicationName,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type AndroidSetupStoreCallbacks = {
stepTransition: AndroidSetupStepTransitionCallback;
setSelectedDevice: (device: DeviceInfo) => void;
setAvailableDevices: (devices: DeviceInfo[]) => void;
getScanPort: () => number | null;
setScanPort: (scanPort?: number) => void;
setApplicationName: (appName?: string) => void;
};
Expand Down
4 changes: 2 additions & 2 deletions src/electron/platform/android/android-service-configurator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export interface AndroidServiceConfigurator {
getPermissionInfo(deviceId: string): Promise<PermissionInfo>;
installService(deviceId: string): Promise<void>;
uninstallService(deviceId: string): Promise<void>;
setTcpForwarding(deviceId: string): Promise<number>;
removeTcpForwarding(deviceId: string): Promise<void>;
setupTcpForwarding(deviceId: string): Promise<number>;
removeTcpForwarding(deviceId: string, hostPort: number): Promise<void>;
}

export interface AndroidServiceConfiguratorFactory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ import {
AppiumAdbCreateParameters,
AppiumAdbCreator,
} from 'electron/platform/android/appium-adb-creator';
import { AppiumServiceConfigurator } from 'electron/platform/android/appium-service-configurator';
import {
AppiumServiceConfigurator,
PortFinder,
} from 'electron/platform/android/appium-service-configurator';

export class AppiumServiceConfiguratorFactory implements AndroidServiceConfiguratorFactory {
public constructor(
private readonly adbCreator: AppiumAdbCreator,
private readonly apkLocator: AndroidServiceApkLocator,
private readonly portFinder: PortFinder,
) {}

public getServiceConfigurator = async (
Expand All @@ -30,6 +34,6 @@ export class AppiumServiceConfiguratorFactory implements AndroidServiceConfigura

const adb: ADB = await this.adbCreator.createADB(parameters);

return new AppiumServiceConfigurator(adb, this.apkLocator);
return new AppiumServiceConfigurator(adb, this.apkLocator, this.portFinder);
};
}
26 changes: 19 additions & 7 deletions src/electron/platform/android/appium-service-configurator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,24 @@ import {
PackageInfo,
PermissionInfo,
} from 'electron/platform/android/android-service-configurator';
import { PortFinderOptions } from 'portfinder';
import { DictionaryStringTo } from 'types/common-types';

type AdbDevice = {
udid: string;
};

export type PortFinder = (options?: PortFinderOptions) => Promise<number>;

const servicePackageName: string = 'com.microsoft.accessibilityinsightsforandroidservice';
export const servicePortNumber: number = 62442; // hardcoded in service APK

export const defaultAdbPortNumber: number = 62442;
export class AppiumServiceConfigurator implements AndroidServiceConfigurator {
constructor(private readonly adb: ADB, private readonly apkLocator: AndroidServiceApkLocator) {}
constructor(
private readonly adb: ADB,
private readonly apkLocator: AndroidServiceApkLocator,
private readonly portFinder: PortFinder,
) {}

public getConnectedDevices = async (): Promise<Array<DeviceInfo>> => {
const detectedDevices: DictionaryStringTo<DeviceInfo> = {};
Expand Down Expand Up @@ -82,14 +89,19 @@ export class AppiumServiceConfigurator implements AndroidServiceConfigurator {
await this.adb.uninstallApk(servicePackageName);
};

public setTcpForwarding = async (deviceId: string): Promise<number> => {
public setupTcpForwarding = async (deviceId: string): Promise<number> => {
const hostPort = await this.portFinder({
port: servicePortNumber,
stopPort: servicePortNumber + 100,
});

this.adb.setDeviceId(deviceId);
await this.adb.forwardPort(defaultAdbPortNumber, defaultAdbPortNumber);
return defaultAdbPortNumber;
await this.adb.forwardPort(hostPort, servicePortNumber);
return hostPort;
};

public removeTcpForwarding = async (deviceId: string): Promise<void> => {
public removeTcpForwarding = async (deviceId: string, hostPort: number): Promise<void> => {
this.adb.setDeviceId(deviceId);
await this.adb.removePortForward(defaultAdbPortNumber);
await this.adb.removePortForward(hostPort);
};
}
3 changes: 2 additions & 1 deletion src/electron/platform/android/setup/android-setup-deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export type AndroidSetupDeps = {
hasExpectedServiceVersion: () => Promise<boolean>;
installService: () => Promise<boolean>;
hasExpectedPermissions: () => Promise<boolean>;
setTcpForwarding: () => Promise<number>;
setupTcpForwarding: () => Promise<number>;
removeTcpForwarding: (hostPort: number) => Promise<void>;
getApplicationName: () => Promise<string>;
logger: Logger;
};
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,12 @@ export class LiveAndroidSetupDeps implements AndroidSetupDeps {
return false;
};

public setTcpForwarding = async (): Promise<number> => {
return await this.serviceConfig.setTcpForwarding(this.selectedDeviceId);
public setupTcpForwarding = async (): Promise<number> => {
return await this.serviceConfig.setupTcpForwarding(this.selectedDeviceId);
};

public removeTcpForwarding = async (hostPort: number): Promise<void> => {
return await this.serviceConfig.removeTcpForwarding(this.selectedDeviceId, hostPort);
};

public getApplicationName = async (): Promise<string> => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ export const configuringPortForwarding: AndroidSetupStepConfig = deps => ({
actions: {},
onEnter: async () => {
try {
deps.setScanPort(null);
deps.setApplicationName(null);
const hostPort = await deps.setTcpForwarding();
const existingPort = deps.getScanPort();
if (existingPort != null) {
deps.logger.log(`removing old tcp:${existingPort} forwarding`);
await deps.removeTcpForwarding(existingPort);
deps.setScanPort(null);
deps.setApplicationName(null);
}

const hostPort = await deps.setupTcpForwarding();
deps.logger.log(`configured forwarding to tcp:${hostPort}`);
const appName = await deps.getApplicationName();

Expand Down
7 changes: 6 additions & 1 deletion src/electron/views/renderer-initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import { PlainTextFormatter } from 'issue-filing/common/markup/plain-text-format
import { IssueFilingServiceProviderForUnifiedImpl } from 'issue-filing/issue-filing-service-provider-for-unified-impl';
import { UnifiedResultToIssueFilingDataConverter } from 'issue-filing/unified-result-to-issue-filing-data';
import { loadTheme, setFocusVisibility } from 'office-ui-fabric-react';
import { getPortPromise } from 'portfinder';
import * as ReactDOM from 'react-dom';
import { ReportExportServiceProviderImpl } from 'report-export/report-export-service-provider-impl';
import { getDefaultAddListenerForCollapsibleSection } from 'reports/components/report-sections/collapsible-script-provider';
Expand Down Expand Up @@ -204,7 +205,11 @@ getPersistedData(indexedDBInstance, indexedDBDataKeysToFetch).then(
androidSetupActions,
createAndroidSetupStateMachineFactory(
new LiveAndroidSetupDeps(
new AppiumServiceConfiguratorFactory(new LiveAppiumAdbCreator(), apkLocator),
new AppiumServiceConfiguratorFactory(
new LiveAppiumAdbCreator(),
apkLocator,
getPortPromise,
),
userConfigurationStore,
apkLocator,
userConfigMessageCreator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@

import { AndroidServiceApkLocator } from 'electron/platform/android/android-service-apk-locator';
import { AppiumAdbCreator } from 'electron/platform/android/appium-adb-creator';
import { AppiumServiceConfigurator } from 'electron/platform/android/appium-service-configurator';
import {
AppiumServiceConfigurator,
PortFinder,
} from 'electron/platform/android/appium-service-configurator';
import { AppiumServiceConfiguratorFactory } from 'electron/platform/android/appium-service-configurator-factory';
import { IMock, Mock, MockBehavior, Times } from 'typemoq';

describe('AppiumServiceConfiguratorFactory tests', () => {
let adbCreatorMock: IMock<AppiumAdbCreator>;
let apkLocatorMock: IMock<AndroidServiceApkLocator>;
let portFinderMock: IMock<PortFinder>;

beforeEach(() => {
adbCreatorMock = Mock.ofType<AppiumAdbCreator>(undefined, MockBehavior.Strict);
apkLocatorMock = Mock.ofType<AndroidServiceApkLocator>(undefined, MockBehavior.Strict);
portFinderMock = Mock.ofType<PortFinder>(undefined, MockBehavior.Strict);
});

it('getServiceConfigurator creates without parameters if no sdkRoot is provided', async () => {
Expand All @@ -24,14 +29,14 @@ describe('AppiumServiceConfiguratorFactory tests', () => {
const factory = new AppiumServiceConfiguratorFactory(
adbCreatorMock.object,
apkLocatorMock.object,
portFinderMock.object,
);

expect(await factory.getServiceConfigurator(null)).toBeInstanceOf(
AppiumServiceConfigurator,
);

adbCreatorMock.verifyAll();
apkLocatorMock.verifyAll();
verifyAllMocks();
});

it('getServiceConfigurator creates with sdkRoot if it is provided', async () => {
Expand All @@ -43,14 +48,14 @@ describe('AppiumServiceConfiguratorFactory tests', () => {
const factory = new AppiumServiceConfiguratorFactory(
adbCreatorMock.object,
apkLocatorMock.object,
portFinderMock.object,
);

expect(await factory.getServiceConfigurator(expectedSdkRoot)).toBeInstanceOf(
AppiumServiceConfigurator,
);

adbCreatorMock.verifyAll();
apkLocatorMock.verifyAll();
verifyAllMocks();
});

it('getServiceConfigurator propagates error to caller', async () => {
Expand All @@ -62,11 +67,17 @@ describe('AppiumServiceConfiguratorFactory tests', () => {
const factory = new AppiumServiceConfiguratorFactory(
adbCreatorMock.object,
apkLocatorMock.object,
portFinderMock.object,
);

await expect(factory.getServiceConfigurator(null)).rejects.toThrowError(expectedMessage);

verifyAllMocks();
});

function verifyAllMocks(): void {
adbCreatorMock.verifyAll();
apkLocatorMock.verifyAll();
});
portFinderMock.verifyAll();
}
});
Loading

0 comments on commit 23db22f

Please sign in to comment.