Skip to content

Commit

Permalink
feat(getport): add experimental "EXP_NET0LISTEN" option
Browse files Browse the repository at this point in the history
this changes the behavior to use "net.listen(0)" to create a random port

re #827
  • Loading branch information
hasezoey committed Nov 9, 2023
1 parent c065147 commit 631ec28
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 10 deletions.
12 changes: 12 additions & 0 deletions docs/api/config-options.md
Expand Up @@ -239,6 +239,18 @@ Also see [ARCHIVE_NAME](#archive_name).
Keep in mind that downloaded binaries will never be automatically deleted.
:::

### EXP_NET0LISTEN

| Environment Variable | PackageJson |
| :------------------: | :---------: |
| `MONGOMS_EXP_NET0LISTEN` | `expNet0Listen` |

Option `EXP_NET0LISTEN` is used to use the experimental (non-predictable) port generation of `net.listen`

This is a experimental option, it maybe removed, renamed or have changed behavior in the future.

Default: `false`

## How to use them in the package.json

To use the config options in the `package.json`, they need to be camelCased (and without `_`), and need to be in the property `config.mongodbMemoryServer`
Expand Down
@@ -1,3 +1,9 @@
import resolveConfig, {
ResolveConfigVariables,
defaultValues,
envToBool,
setDefaultValue,
} from '../../resolveConfig';
import * as getPort from '../index';
import * as net from 'node:net';

Expand Down Expand Up @@ -26,22 +32,28 @@ describe('getport', () => {

describe('tryPort', () => {
it('should return "true" on unused port', async () => {
await expect(getPort.tryPort(20000)).resolves.toStrictEqual(true);
await expect(getPort.tryPort(20000)).resolves.toStrictEqual(20000);
});

it('should return "false" on used port', async () => {
it('should return "-1" on used port', async () => {
const testPort = 30000;
const blockingServer = net.createServer();
blockingServer.unref();
blockingServer.listen(testPort);
await expect(getPort.tryPort(testPort)).resolves.toStrictEqual(false);
await expect(getPort.tryPort(testPort)).resolves.toStrictEqual(-1);
});
});

describe('getFreePort', () => {
const originalResolve = new Map(defaultValues.entries());
beforeEach(() => {
// reset cache to be more consistent in tests
getPort.resetPortsCache();
defaultValues.clear();

for (const [key, val] of originalResolve.entries()) {
defaultValues.set(key, val);
}
});

it('should give a free port from default', async () => {
Expand All @@ -60,6 +72,8 @@ describe('getport', () => {
});

it('port should be predictable', async () => {
expect(envToBool(resolveConfig(ResolveConfigVariables.EXP_NET0LISTEN))).toStrictEqual(false);

const testPort = 23232;
await expect(getPort.getFreePort(testPort)).resolves.toStrictEqual(testPort);

Expand All @@ -74,5 +88,24 @@ describe('getport', () => {

server.close();
});

it('EXP_NET0LISTEN should not be predictable', async () => {
setDefaultValue(ResolveConfigVariables.EXP_NET0LISTEN, 'true');
expect(envToBool(resolveConfig(ResolveConfigVariables.EXP_NET0LISTEN))).toStrictEqual(true);

const testPort = 23232;
await expect(getPort.getFreePort(testPort)).resolves.toStrictEqual(testPort);

const server = await new Promise<net.Server>((res) => {
const server = net.createServer();
server.unref();
server.listen(testPort, () => res(server));
});

const foundPort = await getPort.getFreePort(testPort);
expect(foundPort).not.toStrictEqual(testPort); // not predictable port, so not testable to be a exact number

server.close();
});
});
});
26 changes: 19 additions & 7 deletions packages/mongodb-memory-server-core/src/util/getport/index.ts
@@ -1,3 +1,4 @@
import resolveConfig, { ResolveConfigVariables, envToBool } from '../resolveConfig';
import * as net from 'node:net';

/** Linux min port that does not require root permissions */
Expand Down Expand Up @@ -59,8 +60,15 @@ export async function getFreePort(
while (tries <= max_tries) {
tries += 1;

// use "startPort" at first try, otherwise increase from last number
const nextPort = tries === 1 ? firstPort : validPort(PORTS_CACHE.lastNumber + tries);
let nextPort: number;

if (envToBool(resolveConfig(ResolveConfigVariables.EXP_NET0LISTEN))) {
// "0" means to use ".listen" random port
nextPort = tries === 1 ? firstPort : 0;
} else {
// use "startPort" at first try, otherwise increase from last number
nextPort = tries === 1 ? firstPort : validPort(PORTS_CACHE.lastNumber + tries);
}

// try next port, because it is already in the cache
if (PORTS_CACHE.ports.has(nextPort)) {
Expand All @@ -71,8 +79,10 @@ export async function getFreePort(
// only set "lastNumber" if the "nextPort" was not in the cache
PORTS_CACHE.lastNumber = nextPort;

if (await tryPort(nextPort)) {
return nextPort;
const triedPort = await tryPort(nextPort);

if (triedPort > 0) {
return triedPort;
}
}

Expand All @@ -99,7 +109,7 @@ export function validPort(port: number): number {
* @returns "true" if the port is not in use, "false" if in use
* @throws The error given if the code is not "EADDRINUSE"
*/
export function tryPort(port: number): Promise<boolean> {
export function tryPort(port: number): Promise<number> {
return new Promise((res, rej) => {
const server = net.createServer();

Expand All @@ -113,12 +123,14 @@ export function tryPort(port: number): Promise<boolean> {
rej(err);
}

res(false);
res(-1);
});
server.listen(port, () => {
const address = server.address();
const port = (address as net.AddressInfo).port;
server.close();

res(true);
res(port);
});
});
}
Expand Down
Expand Up @@ -27,6 +27,7 @@ export enum ResolveConfigVariables {
USE_ARCHIVE_NAME_FOR_BINARY_NAME = 'USE_ARCHIVE_NAME_FOR_BINARY_NAME',
MAX_REDIRECTS = 'MAX_REDIRECTS',
DISTRO = 'DISTRO',
EXP_NET0LISTEN = 'EXP_NET0LISTEN',
}

/** The Prefix for Environmental values */
Expand Down

0 comments on commit 631ec28

Please sign in to comment.