Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions detox/src/DetoxWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ class DetoxWorker {
});
}

// @ts-ignore
yield this.device.init();
// @ts-ignore
yield this.device.installUtilBinaries();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
/**
* @typedef {import('../../../common/drivers/android/tools/DeviceHandle')} DeviceHandle
* @typedef {import('../../../common/drivers/android/tools/EmulatorHandle')} EmulatorHandle
*/

const log = require('../../../../utils/logger').child({ cat: 'device' });

const DEVICE_LOOKUP = { event: 'DEVICE_LOOKUP' };

class FreeDeviceFinder {
/**
* @param {import('../../../common/drivers/android/exec/ADB')} adb
* @param {import('../../DeviceRegistry')} deviceRegistry
*/
constructor(adb, deviceRegistry) {
this.adb = adb;
this.deviceRegistry = deviceRegistry;
}

async findFreeDevice(deviceQuery) {
const { devices } = await this.adb.devices();
/**
* @param {DeviceHandle[]} candidates
* @param {string} deviceQuery
* @returns {Promise<import('../../../common/drivers/android/tools/EmulatorHandle') | null>}
*/
async findFreeDevice(candidates, deviceQuery) {
const takenDevices = this.deviceRegistry.getTakenDevicesSync();
for (const candidate of devices) {
for (const candidate of candidates) {
if (await this._isDeviceFreeAndMatching(takenDevices, candidate, deviceQuery)) {
return candidate.adbName;
// @ts-ignore
return candidate;
}
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ const FreeDeviceFinder = require('./FreeDeviceFinder');
const { deviceOffline, emulator5556, ip5557, localhost5555 } = require('./__mocks__/handles');

describe('FreeDeviceFinder', () => {
const mockAdb = { devices: jest.fn() };

/** @type {DeviceList} */
let fakeDeviceList;
let mockDeviceRegistry;
Expand All @@ -17,42 +15,35 @@ describe('FreeDeviceFinder', () => {
mockDeviceRegistry = new DeviceRegistry();
mockDeviceRegistry.getTakenDevicesSync.mockImplementation(() => fakeDeviceList);

const mockAdb = /** @type {any} */ ({});
uut = new FreeDeviceFinder(mockAdb, mockDeviceRegistry);
});

it('should return the only device when it matches, is online and not already taken by other workers', async () => {
mockAdbDevices([emulator5556]);

const result = await uut.findFreeDevice(emulator5556.adbName);
expect(result).toEqual(emulator5556.adbName);
const result = await uut.findFreeDevice([emulator5556], emulator5556.adbName);
expect(result).toEqual(emulator5556);
});

it('should return null when there are no devices', async () => {
mockAdbDevices([]);

const result = await uut.findFreeDevice(emulator5556.adbName);
const result = await uut.findFreeDevice([], emulator5556.adbName);
expect(result).toEqual(null);
});

it('should return null when device is already taken by other workers', async () => {
mockAdbDevices([emulator5556]);
mockAllDevicesTaken();

expect(await uut.findFreeDevice(emulator5556.adbName)).toEqual(null);
expect(await uut.findFreeDevice([emulator5556], emulator5556.adbName)).toEqual(null);
});

it('should return null when device is offline', async () => {
mockAdbDevices([deviceOffline]);
expect(await uut.findFreeDevice(deviceOffline.adbName)).toEqual(null);
expect(await uut.findFreeDevice([deviceOffline], deviceOffline.adbName)).toEqual(null);
});

it('should return first device that matches a regular expression', async () => {
mockAdbDevices([emulator5556, localhost5555, ip5557]);
const localhost = '^localhost:\\d+$';
expect(await uut.findFreeDevice(localhost)).toBe(localhost5555.adbName);
expect(await uut.findFreeDevice([emulator5556, localhost5555, ip5557], localhost)).toBe(localhost5555);
});

const mockAdbDevices = (devices) => mockAdb.devices.mockResolvedValue({ devices });
const mockAllDevicesTaken = () => {
fakeDeviceList.add(emulator5556.adbName, { busy: true });
fakeDeviceList.add(localhost5555.adbName, { busy: true });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ class AttachedAndroidAllocDriver extends AndroidAllocDriver {
*/
async allocate(deviceConfig) {
const adbNamePattern = deviceConfig.device.adbName;
const adbName = await this._deviceRegistry.registerDevice(() => this._freeDeviceFinder.findFreeDevice(adbNamePattern));
const adbName = await this._deviceRegistry.registerDevice(async () =>
(await this._freeDeviceFinder.findFreeDevice(adbNamePattern)).adbName);

return { id: adbName, adbName };
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
/**
* @typedef {import('../../../../common/drivers/android/cookies').AndroidDeviceCookie} AndroidDeviceCookie
* @typedef {import('../../../../common/drivers/android/cookies').EmulatorDeviceCookie} EmulatorDeviceCookie
* @typedef {import('../../../../common/drivers/android/tools/DeviceHandle')} DeviceHandle
*/

const _ = require('lodash');

const log = require('../../../../../utils/logger').child({ cat: 'device,device-allocation' });
const { isPortTaken } = require('../../../../../utils/netUtils');
const adbPortRegistry = require('../../../../common/drivers/android/AdbPortRegistry');
const AndroidAllocDriver = require('../AndroidAllocDriver');

const { patchAvdSkinConfig } = require('./patchAvdSkinConfig');
Expand Down Expand Up @@ -48,29 +52,46 @@ class EmulatorAllocDriver extends AndroidAllocDriver {

/**
* @param deviceConfig
* @returns {Promise<AndroidDeviceCookie>}
* @returns {Promise<EmulatorDeviceCookie>}
*/
async allocate(deviceConfig) {
const avdName = deviceConfig.device.avdName;

await this._avdValidator.validate(avdName, deviceConfig.headless);
await this._fixAvdConfigIniSkinNameIfNeeded(avdName, deviceConfig.headless);

const adbName = await this._deviceRegistry.registerDevice(async () => {
let adbName = await this._freeDeviceFinder.findFreeDevice(avdName);
if (!adbName) {
let adbServerPort;
let adbName;

await this._deviceRegistry.registerDevice(async () => {
const candidates = await this._getAllDevices();
const device = await this._freeDeviceFinder.findFreeDevice(candidates, avdName);

if (device) {
adbName = device.adbName;
adbServerPort = device.adbServerPort;
adbPortRegistry.register(adbName, adbServerPort);
} else {
const port = await this._freePortFinder.findFreePort();
adbName = `emulator-${port}`;

await this._emulatorLauncher.launch({
bootArgs: deviceConfig.bootArgs,
gpuMode: deviceConfig.gpuMode,
headless: deviceConfig.headless,
readonly: deviceConfig.readonly,
avdName,
adbName,
port,
});
adbName = `emulator-${port}`;
adbServerPort = this._getFreeAdbServerPort(candidates);
adbPortRegistry.register(adbName, adbServerPort);

try {
await this._emulatorLauncher.launch({
bootArgs: deviceConfig.bootArgs,
gpuMode: deviceConfig.gpuMode,
headless: deviceConfig.headless,
readonly: deviceConfig.readonly,
avdName,
adbName,
port,
adbServerPort,
});
} catch (e) {
adbPortRegistry.unregister(adbName);
}
}

return adbName;
Expand All @@ -80,6 +101,7 @@ class EmulatorAllocDriver extends AndroidAllocDriver {
id: adbName,
adbName,
name: `${adbName} (${avdName})`,
adbServerPort,
};
}

Expand Down Expand Up @@ -118,7 +140,7 @@ class EmulatorAllocDriver extends AndroidAllocDriver {

async cleanup() {
if (this._shouldShutdown) {
const { devices } = await this._adb.devices();
const devices = await this._getAllDevices();
const actualEmulators = devices.map((device) => device.adbName);
const sessionDevices = await this._deviceRegistry.readSessionDevices();
const emulatorsToShutdown = _.intersection(sessionDevices.getIds(), actualEmulators);
Expand Down Expand Up @@ -146,6 +168,32 @@ class EmulatorAllocDriver extends AndroidAllocDriver {
const binaryVersion = _.get(rawBinaryVersion, 'major');
return await patchAvdSkinConfig(avdName, binaryVersion);
}

/**
* @returns {Promise<DeviceHandle[]>}
* @private
*/
async _getAllDevices() {
const adbServers = await this._getRunningAdbServers();
return (await this._adb.devices({}, adbServers)).devices;
}

/**
* @returns {Promise<number[]>}
* @private
*/
async _getRunningAdbServers() {
const ports = [];
for (let port = this._adb.defaultServerPort + 1; await isPortTaken(port); port++) {
ports.push(port);
}
return ports;
}

_getFreeAdbServerPort(currentDevices) {
const maxPortDevice = _.maxBy(currentDevices, 'adbServerPort');
return _.get(maxPortDevice, 'adbServerPort', this._adb.defaultServerPort) + 1;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you check that it is actually free?

}
}

module.exports = EmulatorAllocDriver;
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class EmulatorLauncher {
* @param {object} options
* @param {string} options.avdName
* @param {string} options.adbName
* @param {number} options.adbServerPort
* @param {number} options.port
* @param {string | undefined} options.bootArgs
* @param {string | undefined} options.gpuMode
Expand All @@ -29,7 +30,7 @@ class EmulatorLauncher {
retries: 2,
interval: 100,
conditionFn: isUnknownEmulatorError,
}, () => launchEmulatorProcess(this._emulatorExec, this._adb, launchCommand));
}, () => launchEmulatorProcess(this._emulatorExec, this._adb, launchCommand, options.adbServerPort));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('Emulator launcher', () => {
expect(launchEmulatorProcess).toHaveBeenCalledWith(emulatorExec, adb, expect.objectContaining({
avdName,
adbName,
}));
}), undefined);
});

it('should launch using a specific emulator port, if provided', async () => {
Expand All @@ -49,6 +49,13 @@ describe('Emulator launcher', () => {
expect(command.port).toEqual(port);
});

it('should pass adbServerPort to launchEmulatorProcess', async () => {
const adbServerPort = 5038;
await uut.launch({ avdName, adbName, adbServerPort });

expect(launchEmulatorProcess).toHaveBeenCalledWith(emulatorExec, adb, expect.anything(), adbServerPort);
});

it('should retry emulator process launching with custom args', async () => {
const expectedRetryOptions = {
retries: 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ const DeviceList = require('../../../DeviceList');
const { emulator5556, localhost5555, mockAvdName } = require('../__mocks__/handles');

describe('FreeEmulatorFinder', () => {
const mockAdb = { devices: jest.fn() };

/** @type {DeviceList} */
let fakeDeviceList;
/** @type {jest.Mocked<import('../../../DeviceRegistry')>} */
Expand All @@ -18,24 +16,20 @@ describe('FreeEmulatorFinder', () => {
mockDeviceRegistry.getTakenDevicesSync.mockImplementation(() => fakeDeviceList);

const FreeEmulatorFinder = require('./FreeEmulatorFinder');
const mockAdb = /** @type {any} */ ({});
uut = new FreeEmulatorFinder(mockAdb, mockDeviceRegistry);
});

it('should return device when it is an emulator and avdName matches', async () => {
mockAdbDevices([emulator5556]);
const result = await uut.findFreeDevice(mockAvdName);
expect(result).toBe(emulator5556.adbName);
const result = await uut.findFreeDevice([emulator5556], mockAvdName);
expect(result).toBe(emulator5556);
});

it('should return null when avdName does not match', async () => {
mockAdbDevices([emulator5556]);
expect(await uut.findFreeDevice('wrongAvdName')).toBe(null);
expect(await uut.findFreeDevice([emulator5556], 'wrongAvdName')).toBe(null);
});

it('should return null when not an emulator', async () => {
mockAdbDevices([localhost5555]);
expect(await uut.findFreeDevice(mockAvdName)).toBe(null);
expect(await uut.findFreeDevice([localhost5555], mockAvdName)).toBe(null);
});

const mockAdbDevices = (devices) => mockAdb.devices.mockResolvedValue({ devices });
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const net = require('net');
const { isPortTaken } = require('../../../../../utils/netUtils');

class FreePortFinder {
constructor({ min = 10000, max = 20000 } = {}) {
Expand All @@ -14,24 +14,10 @@ class FreePortFinder {
const max = this._max;
port = Math.random() * (max - min) + min;
port = port & 0xFFFFFFFE; // Should always be even
} while (await this.isPortTaken(port));
} while (await isPortTaken(port));

return port;
}

async isPortTaken(port) {
return new Promise((resolve, reject) => {
const tester = net.createServer()
.once('error', /** @param {*} err */ (err) => {
/* istanbul ignore next */
return err && err.code === 'EADDRINUSE' ? resolve(true) : reject(err);
})
.once('listening', () => {
tester.once('close', () => resolve(false)).close();
})
.listen(port);
});
}
}

module.exports = FreePortFinder;
Original file line number Diff line number Diff line change
@@ -1,39 +1,21 @@
const net = require('net');

const FreePortFinder = require('./FreePortFinder');

describe('FreePortFinder', () => {
let finder;
let server;
let isPortTaken;

beforeEach(() => {
finder = new FreePortFinder();
});
jest.mock('../../../../../utils/netUtils');
isPortTaken = require('../../../../../utils/netUtils').isPortTaken;
isPortTaken.mockResolvedValue(false);

afterEach(done => {
if (server) {
server.close(done);
} else {
done();
}
const FreePortFinder = require('./FreePortFinder');
finder = new FreePortFinder();
});

test('should find a free port', async () => {
const port = await finder.findFreePort();
expect(port).toBeGreaterThanOrEqual(10000);
expect(port).toBeLessThanOrEqual(20000);
expect(port % 2).toBe(0);
await expect(finder.isPortTaken(port)).resolves.toBe(false);
});

test('should identify a taken port', async () => {
server = net.createServer();
const portTaken = await new Promise(resolve => {
server.listen(0, () => { // 0 means random available port
resolve(server.address().port);
});
});

await expect(finder.isPortTaken(portTaken)).resolves.toBe(true);
expect(isPortTaken).toHaveBeenCalled();
});
});
Loading