-
-
Notifications
You must be signed in to change notification settings - Fork 186
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
194 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
This sub-module is inspired by `get-port`, which started shipping ESM-only builds and we werent able to upgrade yet, also to address memory leaks (not using intervals or timeouts) |
62 changes: 62 additions & 0 deletions
62
packages/mongodb-memory-server-core/src/util/getport/__tests__/getport.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import * as getPort from '../index'; | ||
import * as http from 'node:http'; | ||
|
||
// the following tests may fail on systems with actual ports being used in those ranges (20000 to 40000) | ||
|
||
describe('getport', () => { | ||
describe('validPort', () => { | ||
it('should return input unmodified if valid', () => { | ||
expect(getPort.validPort(2000)).toStrictEqual(2000); | ||
}); | ||
|
||
it('should return minimal amount if below', () => { | ||
expect(getPort.validPort(0)).toStrictEqual(getPort.MIN_PORT); | ||
}); | ||
|
||
it('should return wrap around if over max port', () => { | ||
expect(getPort.validPort(getPort.MAX_PORT + 10)).toStrictEqual(getPort.MIN_PORT); | ||
}); | ||
|
||
it('should return wrap around if over max port more than MIN', () => { | ||
expect(getPort.validPort(getPort.MAX_PORT + getPort.MIN_PORT + 10)).toStrictEqual( | ||
getPort.MIN_PORT + 10 | ||
); | ||
}); | ||
}); | ||
|
||
describe('tryPort', () => { | ||
it('should return "true" on unused port', async () => { | ||
await expect(getPort.tryPort(20000)).resolves.toStrictEqual(true); | ||
}); | ||
|
||
it('should return "false" on used port', async () => { | ||
const testPort = 30000; | ||
const blockingServer = http.createServer(); | ||
blockingServer.unref(); | ||
blockingServer.listen(testPort); | ||
await expect(getPort.tryPort(testPort)).resolves.toStrictEqual(false); | ||
}); | ||
}); | ||
|
||
describe('getFreePort', () => { | ||
beforeEach(() => { | ||
// reset cache to be more consistent in tests | ||
getPort.resetPortsCache(); | ||
}); | ||
|
||
it('should give a free port from default', async () => { | ||
await expect(getPort.getFreePort()).resolves.toBeTruthy(); | ||
}); | ||
|
||
it('should respect max_tries', async () => { | ||
const testPort = 40000; | ||
await expect(getPort.getFreePort(testPort, 0)).resolves.toBeTruthy(); | ||
await expect(getPort.getFreePort(testPort, 0)).rejects.toBeTruthy(); | ||
}); | ||
|
||
it('should return start port', async () => { | ||
const testPort = 23000; | ||
await expect(getPort.getFreePort(testPort)).resolves.toStrictEqual(testPort); | ||
}); | ||
}); | ||
}); |
131 changes: 131 additions & 0 deletions
131
packages/mongodb-memory-server-core/src/util/getport/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import * as http from 'node:http'; | ||
|
||
/** Linux min port that does not require root permissions */ | ||
export const MIN_PORT = 1024; | ||
/** u16 max number */ | ||
export const MAX_PORT = 65535; | ||
|
||
/** Storage for the ports that were already tried */ | ||
interface IPortsCache { | ||
/** The time the set was last used */ | ||
timeSet: undefined | number; | ||
/** The ports that were tried */ | ||
ports: Set<number>; | ||
/** Store last used number, reduces amount of tries needed */ | ||
lastNumber: number; | ||
} | ||
|
||
/** | ||
* Time before {@link PORTS_CACHE} gets cleared | ||
* 10 seconds | ||
*/ | ||
const PORTS_CACHE_CLEAN_TIME = 1000 * 10; | ||
|
||
/** | ||
* Ports cache, so that locked ports are quickly ignored and hoping for less port stealing | ||
*/ | ||
const PORTS_CACHE: IPortsCache = { | ||
timeSet: undefined, | ||
ports: new Set(), | ||
lastNumber: MIN_PORT, | ||
}; | ||
|
||
/** Max default tries before giving up */ | ||
const MAX_DEFAULT_TRIES = 10; | ||
|
||
/** | ||
* Try to get a free port | ||
* @param firstPort The first port to try or empty for semi-random port | ||
* @param max_tries maximum amount of tries to get a port, default to {@link MAX_DEFAULT_TRIES} | ||
* @returns A valid free port | ||
* @throws if "max_tries" is exceeded | ||
*/ | ||
export async function getFreePort( | ||
firstPort?: number, | ||
max_tries: number = MAX_DEFAULT_TRIES | ||
): Promise<number> { | ||
// use "Date" as a semi-random value to lessen conflicts between simultaneous tests | ||
firstPort = firstPort || validPort(Date.now()); | ||
|
||
// clear ports cache after some time, but not on a interval | ||
if (PORTS_CACHE.timeSet && Date.now() - PORTS_CACHE.timeSet > PORTS_CACHE_CLEAN_TIME) { | ||
PORTS_CACHE.ports.clear(); | ||
PORTS_CACHE.timeSet = Date.now(); | ||
} else if (!PORTS_CACHE.timeSet) { | ||
PORTS_CACHE.timeSet = Date.now(); | ||
} | ||
|
||
let tries = 0; | ||
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); | ||
|
||
// try next port, because it is already in the cache | ||
if (PORTS_CACHE.ports.has(nextPort)) { | ||
continue; | ||
} | ||
|
||
PORTS_CACHE.ports.add(nextPort); | ||
// only set "lastNumber" if the "nextPort" was not in the cache | ||
PORTS_CACHE.lastNumber = nextPort; | ||
|
||
if (await tryPort(nextPort)) { | ||
return nextPort; | ||
} | ||
} | ||
|
||
throw new Error('Max port tries exceeded'); | ||
} | ||
|
||
export default getFreePort; | ||
|
||
/** | ||
* Check that input number is within range of {@link MIN_PORT} and {@link MAX_PORT} | ||
* If more than {@link MAX_PORT}, wrap around, if less than {@link MIN_PORT} use {@link MIN_PORT} | ||
* @param port The Number to check | ||
* @returns A Valid number in port range | ||
*/ | ||
export function validPort(port: number): number { | ||
const mod = port % MAX_PORT; | ||
|
||
return mod < MIN_PORT ? MIN_PORT : mod; | ||
} | ||
|
||
/** | ||
* Try a given port | ||
* @param port The port to try | ||
* @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> { | ||
return new Promise((res, rej) => { | ||
const server = http.createServer(); | ||
server.unref(); // dont keep this server from exiting the application | ||
|
||
server.on('error', (err) => { | ||
if ((err as any)?.code !== 'EADDRINUSE') { | ||
rej(err); | ||
} | ||
|
||
res(false); | ||
}); | ||
server.listen(port, () => { | ||
server.close(); | ||
|
||
res(true); | ||
}); | ||
}); | ||
} | ||
|
||
/** | ||
* Reset the {@link PORTS_CACHE} to its initial state | ||
* | ||
* This function is meant for debugging and testing purposes only | ||
*/ | ||
export function resetPortsCache() { | ||
PORTS_CACHE.lastNumber = MIN_PORT; | ||
PORTS_CACHE.timeSet = undefined; | ||
PORTS_CACHE.ports.clear(); | ||
} |