Skip to content

Commit 2b0030e

Browse files
fix: make port management better (#1926)
1 parent a81c920 commit 2b0030e

File tree

4 files changed

+155
-6
lines changed

4 files changed

+155
-6
lines changed

src/CapabilityManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export async function androidCapabilities(
5959
caps.firstMatch[0]['appium:app'] = await findAppPath(caps);
6060
caps.firstMatch[0]['appium:udid'] = freeDevice.udid;
6161
caps.firstMatch[0]['appium:systemPort'] = await getFreePort(options.portRange);
62-
caps.firstMatch[0]['appium:chromeDriverPort'] = await getPort();
62+
caps.firstMatch[0]['appium:chromeDriverPort'] = await getFreePort(options.portRange);
6363
caps.firstMatch[0]['appium:adbRemoteHost'] = freeDevice.adbRemoteHost;
6464
caps.firstMatch[0]['appium:adbPort'] = freeDevice.adbPort;
6565
if (freeDevice.chromeDriverPath)

src/device-utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import DevicePlatform from './enums/Platform';
3434
import {
3535
cachePath,
3636
checkIfPathIsAbsolute,
37+
getFreePort,
3738
isAppiumRunningAt,
3839
isDeviceFarmRunning,
3940
isMac,
@@ -241,7 +242,7 @@ export async function updateCapabilityForDevice(
241242

242243
if (!device.hasOwnProperty('cloud')) {
243244
if (mergedCapabilites['appium:automationName']?.toLowerCase() === 'flutterintegration') {
244-
capability.firstMatch[0]['appium:flutterSystemPort'] = await getPort();
245+
capability.firstMatch[0]['appium:flutterSystemPort'] = await getFreePort(options.portRange);
245246
}
246247

247248
if (device.platform.toLowerCase() == DevicePlatform.ANDROID) {

src/helpers.ts

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,53 @@ import { FakeModuleLoader } from './fake-module-loader';
1515
import { IExternalModuleLoader } from './interfaces/IExternalModule';
1616

1717
const APPIUM_VENDOR_PREFIX = 'appium:';
18+
19+
// Port manager to track allocated ports
20+
class PortManager {
21+
private allocatedPorts: Set<number> = new Set();
22+
23+
/**
24+
* Check if a port is already allocated
25+
*/
26+
isPortAllocated(port: number): boolean {
27+
return this.allocatedPorts.has(port);
28+
}
29+
30+
/**
31+
* Allocate a port by adding it to the allocated set
32+
*/
33+
allocatePort(port: number): void {
34+
if (port && port > 0) {
35+
this.allocatedPorts.add(port);
36+
log.debug(`Port ${port} has been allocated`);
37+
}
38+
}
39+
40+
/**
41+
* Release a port by removing it from the allocated set
42+
*/
43+
releasePort(port: number): void {
44+
if (port && port > 0 && this.allocatedPorts.has(port)) {
45+
this.allocatedPorts.delete(port);
46+
log.debug(`Port ${port} has been released`);
47+
}
48+
}
49+
50+
/**
51+
* Release multiple ports
52+
*/
53+
releasePorts(ports: (number | undefined | null)[]): void {
54+
ports.forEach((port) => {
55+
if (port && port > 0) {
56+
this.releasePort(port);
57+
}
58+
});
59+
}
60+
}
61+
62+
// Singleton instance of port manager
63+
const portManager = new PortManager();
64+
1865
export async function asyncForEach(
1966
array: string | any[],
2067
callback: {
@@ -69,16 +116,65 @@ export function checkIfPathIsAbsolute(configPath: string) {
69116
return path.isAbsolute(configPath);
70117
}
71118

72-
export async function getFreePort(portRange?: string) {
119+
export async function getFreePort(portRange?: string): Promise<number> {
120+
let port: number;
121+
73122
if (portRange) {
74123
const range = portRange.split('-').map(Number);
75124
if (range.length !== 2 || isNaN(range[0]) || isNaN(range[1]) || range[0] > range[1]) {
76125
log.warn(`Invalid port range format: "${portRange}". Falling back to any free port.`);
126+
port = await getPort();
127+
// Check if allocated and try again if needed
128+
while (portManager.isPortAllocated(port)) {
129+
port = await getPort();
130+
}
77131
} else {
78-
return await getPort({ port: getPort.makeRange(range[0], range[1]) });
132+
// Convert iterable to array and filter out allocated ports upfront
133+
const portOptions = Array.from(getPort.makeRange(range[0], range[1]));
134+
const availablePorts = portOptions.filter((p: number) => !portManager.isPortAllocated(p));
135+
136+
if (availablePorts.length === 0) {
137+
log.warn(`All ports in range ${portRange} are allocated. Falling back to any free port.`);
138+
port = await getPort();
139+
// Check if allocated and try again if needed
140+
while (portManager.isPortAllocated(port)) {
141+
port = await getPort();
142+
}
143+
} else {
144+
// Get a free port from the available (non-allocated) ports
145+
// getPort will check system usage among these available ports
146+
port = await getPort({ port: availablePorts });
147+
}
148+
}
149+
} else {
150+
port = await getPort();
151+
// Check if the port is already allocated, keep trying until we find one that's not
152+
while (portManager.isPortAllocated(port)) {
153+
port = await getPort();
79154
}
80155
}
81-
return await getPort();
156+
157+
// Allocate the port before returning it
158+
portManager.allocatePort(port);
159+
return port;
160+
}
161+
162+
/**
163+
* Release a port that was previously allocated
164+
* @param port - The port number to release
165+
*/
166+
export function releasePort(port: number | undefined | null): void {
167+
if (port && port > 0) {
168+
portManager.releasePort(port);
169+
}
170+
}
171+
172+
/**
173+
* Release multiple ports that were previously allocated
174+
* @param ports - Array of port numbers to release
175+
*/
176+
export function releasePorts(ports: (number | undefined | null)[]): void {
177+
portManager.releasePorts(ports);
82178
}
83179

84180
export function nodeUrl(device: IDevice, basePath = ''): string {

src/plugin.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ import _ from 'lodash';
6060
import { DeviceFarmApiClient } from './api-client';
6161
import { getDeviceFarmCapabilities } from './CapabilityManager';
6262
import { config, config as pluginConfig } from './config';
63-
import { getFreePort } from './helpers';
63+
import { getFreePort, releasePorts } from './helpers';
6464
import { ATDRepository } from './data-service/db';
6565
import { NodeService } from './data-service/node-service';
6666
import { addCLIArgs } from './data-service/pluginArgs';
@@ -716,6 +716,58 @@ class DevicePlugin extends BasePlugin {
716716
log.warn(`Error while releasing connection for device ${device.udid}. Error: ${err}`);
717717
}
718718
}
719+
720+
// Collect all ports used by the device and release them
721+
const portsToRelease: (number | undefined | null)[] = [];
722+
723+
// Ports from device object
724+
if (device) {
725+
// iOS ports
726+
if (device.wdaLocalPort) portsToRelease.push(device.wdaLocalPort);
727+
if (device.mjpegServerPort) portsToRelease.push(device.mjpegServerPort);
728+
if (device.goIOSAgentPort) portsToRelease.push(device.goIOSAgentPort);
729+
730+
// Android ports (stored on device if available)
731+
if (device.systemPort) portsToRelease.push(device.systemPort);
732+
733+
// Ports from sessionResponse capabilities (Android and iOS)
734+
// sessionResponse contains the capabilities returned from Appium
735+
if (device.sessionResponse) {
736+
const sessionResponse = device.sessionResponse;
737+
// Check for ports with appium: prefix
738+
const capabilities = sessionResponse.capabilities || sessionResponse;
739+
740+
// Android ports
741+
const systemPort = capabilities['appium:systemPort'] || capabilities.systemPort;
742+
const chromeDriverPort =
743+
capabilities['appium:chromeDriverPort'] || capabilities.chromeDriverPort;
744+
const flutterSystemPort =
745+
capabilities['appium:flutterSystemPort'] || capabilities.flutterSystemPort;
746+
747+
// iOS ports
748+
const wdaLocalPort = capabilities['appium:wdaLocalPort'] || capabilities.wdaLocalPort;
749+
750+
// Common ports
751+
const mjpegServerPort =
752+
capabilities['appium:mjpegServerPort'] || capabilities.mjpegServerPort;
753+
754+
if (systemPort) portsToRelease.push(systemPort);
755+
if (chromeDriverPort) portsToRelease.push(chromeDriverPort);
756+
if (flutterSystemPort) portsToRelease.push(flutterSystemPort);
757+
if (wdaLocalPort) portsToRelease.push(wdaLocalPort);
758+
if (mjpegServerPort) portsToRelease.push(mjpegServerPort);
759+
}
760+
}
761+
762+
// Release all collected ports
763+
const validPorts = portsToRelease.filter((p) => p !== null && p !== undefined);
764+
if (validPorts.length > 0) {
765+
releasePorts(validPorts);
766+
log.info(
767+
`📱 Released ${validPorts.length} port(s) for session ${sessionId}: ${validPorts.join(', ')}`,
768+
);
769+
}
770+
719771
return res;
720772
}
721773
}

0 commit comments

Comments
 (0)