Skip to content

Commit a7f101b

Browse files
jscascaTheSpyder
andauthored
TINY-13630: Remove race condition from port binding (#162)
* Refactored our routes so the server can start as soon as the port has been checked * TINY-13630: Parallel port binding * TINY-13630: Fix manual port selection --------- Co-authored-by: Andrew Herron <thespyder@programmer.net>
1 parent 45729ce commit a7f101b

File tree

20 files changed

+320
-263
lines changed

20 files changed

+320
-263
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## Unreleased
8+
9+
### Changed
10+
- `bedrock-auto` now binds to a random port to avoid race conditions. #TINY-13630
11+
- Compiled tests moved to custom scratch folder. #TINY-13630
12+
713
## 15.2.0 - 2026-01-30
814

915
## Changed

Jenkinsfile

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
standardProperties()
77

88
timestamps {
9-
tinyPods.node(
9+
tinyPods.nodeBrowser(
1010
tag: '20',
11-
resourceRequestMemory: '2Gi',
12-
resourceLimitMemory: '2Gi'
11+
resourceRequestMemory: '4Gi',
12+
resourceLimitMemory: '4Gi'
1313
) {
1414
stage("clean") {
1515
exec('yarn clean')
@@ -26,43 +26,38 @@ timestamps {
2626
stage("test") {
2727
exec('yarn test')
2828
}
29-
}
3029

31-
// Testing
32-
stage("bedrock testing") {
33-
bedrockRemoteBrowsers(
34-
testContainer: [
35-
resourceRequestMemory: '2Gi',
36-
resourceLimitMemory: '2Gi',
37-
],
38-
platforms: [
39-
[ browser: 'chrome', provider: 'aws', buckets: 2 ],
40-
[ browser: 'firefox', provider: 'aws', buckets: 2 ],
41-
[ browser: 'edge', provider: 'lambdatest', buckets: 1 ],
42-
[ browser: 'chrome', provider: 'lambdatest', os: 'macOS Sonoma', buckets: 1 ],
43-
[ browser: 'firefox', provider: 'lambdatest', os: 'macOS Sonoma', buckets: 1 ],
44-
[ browser: 'safari', provider: 'lambdatest', os: 'macOS Sonoma', buckets: 1 ],
45-
],
46-
prepareTests: {
47-
yarnInstall()
48-
sh 'yarn build'
49-
},
30+
def platforms = [
31+
[ browser: 'chrome', provider: 'aws', buckets: 2 ],
32+
[ browser: 'firefox', provider: 'aws', buckets: 2 ],
33+
[ browser: 'edge', provider: 'lambdatest' ],
34+
[ browser: 'chrome', provider: 'lambdatest', os: 'macOS Sonoma' ],
35+
[ browser: 'firefox', provider: 'lambdatest', os: 'macOS Sonoma' ],
36+
[ browser: 'safari', provider: 'lambdatest', os: 'macOS Sonoma' ],
37+
[ browser: 'chrome', provider: 'headless' ]
38+
]
39+
40+
def cleanBranchName = (env.BRANCH_NAME ?: "").split('/').last()
41+
def testPrefix = "Bedrock_${cleanBranchName}-b${env.BUILD_NUMBER}"
42+
43+
bedrockLocalBrowsers(
5044
testDirs: [ 'modules/sample/src/test/ts/**/pass' ],
51-
custom: '--config modules/sample/tsconfig.json --customRoutes modules/sample/routes.json'
45+
custom: '--config modules/sample/tsconfig.json --customRoutes modules/sample/routes.json',
46+
platforms: platforms,
47+
prefix: testPrefix
5248
)
53-
}
5449

55-
// Publish
56-
if (isReleaseBranch()) {
57-
stage("publish") {
58-
tinyPods.node() {
59-
yarnInstall()
60-
sh 'yarn build'
61-
tinyNpm.withNpmPublishCredentials {
62-
// We need to tell git to ignore the changes to .npmrc when publishing
63-
exec('git update-index --assume-unchanged .npmrc')
64-
// Re-evaluate whether we still need the `--no-verify-access` flag after upgrading Lerna (TINY-13539)
65-
exec('yarn lerna publish from-package --yes --no-git-reset --ignore @ephox/bedrock-sample --no-verify-access')
50+
if (isReleaseBranch()) {
51+
stage("publish") {
52+
tinyPods.node() {
53+
yarnInstall()
54+
sh 'yarn build'
55+
tinyNpm.withNpmPublishCredentials {
56+
// We need to tell git to ignore the changes to .npmrc when publishing
57+
exec('git update-index --assume-unchanged .npmrc')
58+
// Re-evaluate whether we still need the `--no-verify-access` flag after upgrading Lerna (TINY-13539)
59+
exec('yarn lerna publish from-package --yes --no-git-reset --ignore @ephox/bedrock-sample --no-verify-access')
60+
}
6661
}
6762
}
6863
}

modules/sample/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
"author": "Tiny Technologies Inc",
55
"license": "Apache-2.0",
66
"scripts": {
7-
"bedrock": "node ../server/bin/bedrock.js",
8-
"bedrock-auto": "node ../server/bin/bedrock-auto.js",
7+
"bedrock": "node ../server/bin/bedrock.cjs",
8+
"bedrock-auto": "node ../server/bin/bedrock-auto.cjs",
99
"test-samples-pass": "bedrock-auto -b chrome-headless --config tsconfig.json --customRoutes routes.json -d src/test/ts/**/pass",
1010
"test-samples-only": "bedrock-auto -b chrome-headless --config tsconfig.json -d src/test/ts/**/only",
1111
"test-samples-pass-js": "bedrock-auto -b chrome-headless -d src/test/js/**/pass",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env node
2+
3+
const BedrockCli = require("../lib/main/ts/BedrockCli");
4+
const BedrockAuto = require("../lib/main/ts/BedrockAuto");
5+
6+
(async () => {
7+
await BedrockCli.run(BedrockAuto, {
8+
current: process.cwd(),
9+
bin: __dirname,
10+
});
11+
})().catch((err) => {
12+
console.error(err);
13+
process.exit(1);
14+
});

modules/server/bin/bedrock-auto.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

modules/server/bin/bedrock.cjs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env node
2+
3+
const BedrockCli = require("../lib/main/ts/BedrockCli");
4+
const BedrockManual = require("../lib/main/ts/BedrockManual");
5+
6+
(async () => {
7+
await BedrockCli.run(BedrockManual, {
8+
current: process.cwd(),
9+
bin: __dirname,
10+
});
11+
})().catch((err) => {
12+
console.error(err);
13+
process.exit(1);
14+
});

modules/server/bin/bedrock.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

modules/server/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
"author": "Tiny Technologies Inc",
55
"license": "Apache-2.0",
66
"bin": {
7-
"bedrock-auto": "./bin/bedrock-auto.js",
8-
"bedrock": "./bin/bedrock.js"
7+
"bedrock-auto": "./bin/bedrock-auto.cjs",
8+
"bedrock": "./bin/bedrock.cjs"
99
},
1010
"scripts": {
1111
"prepublishOnly": "tsc -b",

modules/server/src/main/ts/BedrockAuto.ts

Lines changed: 70 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,58 @@ import { BedrockAutoSettings } from './bedrock/core/Settings';
1212
import { ExitCodes } from './bedrock/util/ExitCodes';
1313
import * as ConsoleReporter from './bedrock/core/ConsoleReporter';
1414
import * as SettingsResolver from './bedrock/core/SettingsResolver';
15-
import * as portfinder from 'portfinder';
1615
import { format } from 'node:util';
16+
import { Browser } from 'webdriverio';
17+
import { defer } from './bedrock/util/Waiter';
1718

18-
export const go = (bedrockAutoSettings: BedrockAutoSettings): void => {
19+
async function makeWebDriver(settings: BedrockAutoSettings, servicePort: number, shutdownServices: ((immediate?: boolean) => Promise<void>)[], browserName: string, isHeadless: boolean) {
20+
// Remote settings
21+
const remoteWebdriver = settings.remote;
22+
const sishDomain = settings.sishDomain;
23+
const username = settings.username ?? process.env.LT_USERNAME;
24+
const accesskey = settings.accesskey ?? process.env.LT_ACCESS_KEY;
25+
const tunnelCredentials = {
26+
user: username,
27+
key: accesskey
28+
};
29+
30+
const tunnel = await Tunnel.prepareConnection(servicePort, remoteWebdriver, sishDomain, tunnelCredentials);
31+
shutdownServices.push(tunnel.shutdown);
32+
const location = tunnel.url.href;
33+
34+
console.log(`Creating ${ remoteWebdriver ?? 'local' } webdriver...`);
35+
if (remoteWebdriver == 'aws') {
36+
console.log('INFO: Webdriver creation waits for device farm session to activate. Takes 30-45s.');
37+
}
38+
39+
const driver = await Driver.create({
40+
browser: browserName,
41+
basedir: settings.basedir,
42+
headless: isHeadless,
43+
debuggingPort: settings.debuggingPort,
44+
useSandboxForHeadless: settings.useSandboxForHeadless,
45+
extraBrowserCapabilities: settings.extraBrowserCapabilities,
46+
verbose: settings.verbose,
47+
wipeBrowserCache: settings.wipeBrowserCache,
48+
remoteWebdriver,
49+
webdriverPort: settings.webdriverPort,
50+
useSelenium: settings.useSelenium,
51+
username,
52+
accesskey,
53+
devicefarmRegion: settings.devicefarmRegion,
54+
deviceFarmArn: settings.devicefarmArn,
55+
browserVersion: settings.browserVersion,
56+
platformName: settings.platformName,
57+
tunnel,
58+
name: settings.name ? settings.name : 'bedrock-auto'
59+
});
60+
shutdownServices.push(driver.shutdown);
61+
62+
const webdriver = driver.webdriver;
63+
return { location, webdriver };
64+
}
65+
66+
export const go = async (bedrockAutoSettings: BedrockAutoSettings): Promise<void> => {
1967
console.log('bedrock-auto ' + Version.get() + ' starting...');
2068

2169
const settings = SettingsResolver.resolveAndLog(bedrockAutoSettings);
@@ -24,72 +72,36 @@ export const go = (bedrockAutoSettings: BedrockAutoSettings): void => {
2472
const isPhantom = browserName === 'phantomjs';
2573
const isHeadless = settings.browser.endsWith('-headless') || isPhantom;
2674
const basePage = 'src/resources/html/' + (isPhantom ? 'bedrock-phantom.html' : 'bedrock.html');
27-
// Remote settings
28-
const remoteWebdriver = settings.remote;
29-
const sishDomain = settings.sishDomain;
30-
const username = settings.username ?? process.env.LT_USERNAME;
31-
const accesskey = settings.accesskey ?? process.env.LT_ACCESS_KEY;
32-
33-
const routes = RunnerRoutes.generate('auto', settings.projectdir, settings.basedir, settings.config, settings.bundler, settings.testfiles, settings.chunk, settings.retries, settings.singleTimeout, settings.stopOnFailure, basePage, settings.coverage, settings.polyfills);
3475

3576
const shutdownServices: ((immediate?: boolean) => Promise<void>)[] = [];
3677
const shutdown = (services: ((immediate?: boolean) => Promise<void>)[]) => (immediate?: boolean) => Promise.allSettled(services.map((fn) => fn(immediate)));
3778

38-
routes.then(async (runner) => {
39-
40-
// LambdaTest Tunnel must know dev server port, but tunnel must be created before dev server.
41-
const servicePort = await portfinder.getPortPromise({
42-
port: 8000,
43-
stopPort: 20000
44-
});
45-
46-
const tunnelCredentials = {
47-
user: username,
48-
key: accesskey
49-
};
79+
try {
5080

51-
const tunnel = await Tunnel.prepareConnection(servicePort, remoteWebdriver, sishDomain, tunnelCredentials);
52-
shutdownServices.push(tunnel.shutdown);
53-
const location = tunnel.url.href;
81+
const driverDeferred = defer<Attempt<unknown, Browser>>();
5482

55-
console.log(`Creating ${remoteWebdriver ?? 'local'} webdriver...`);
56-
if (remoteWebdriver == 'aws') {
57-
console.log('INFO: Webdriver creation waits for device farm session to activate. Takes 30-45s.');
58-
}
83+
const scratchDir = settings.name ? `scratch_${settings.name}` : `bedrock`;
5984

60-
const driver = await Driver.create({
61-
browser: browserName,
62-
basedir: settings.basedir,
63-
headless: isHeadless,
64-
debuggingPort: settings.debuggingPort,
65-
useSandboxForHeadless: settings.useSandboxForHeadless,
66-
extraBrowserCapabilities: settings.extraBrowserCapabilities,
67-
verbose: settings.verbose,
68-
wipeBrowserCache: settings.wipeBrowserCache,
69-
remoteWebdriver,
70-
webdriverPort: settings.webdriverPort,
71-
useSelenium: settings.useSelenium,
72-
username,
73-
accesskey,
74-
devicefarmRegion: settings.devicefarmRegion,
75-
deviceFarmArn: settings.devicefarmArn,
76-
browserVersion: settings.browserVersion,
77-
platformName: settings.platformName,
78-
tunnel,
79-
name: settings.name ? settings.name : 'bedrock-auto'
80-
});
85+
const routesPromise = RunnerRoutes.generate('auto', settings.projectdir, settings.basedir, scratchDir, settings.config, settings.bundler, settings.testfiles, settings.chunk, settings.retries, settings.singleTimeout, settings.stopOnFailure, basePage, settings.coverage, settings.polyfills);
8186

82-
const webdriver = driver.webdriver;
83-
console.log('Started webdriver session: ', webdriver.sessionId);
8487
const service = await Serve.start({
8588
...settings,
86-
driver: Attempt.passed(webdriver),
89+
driver: driverDeferred.promise,
8790
master,
88-
runner,
91+
runner: routesPromise,
8992
stickyFirstSession: true,
90-
port: servicePort
9193
});
92-
shutdownServices.push(service.shutdown, driver.shutdown);
94+
const driverPromise = makeWebDriver(settings, service.port, shutdownServices, browserName, isHeadless);
95+
driverPromise.then(({ webdriver }) => {
96+
driverDeferred.resolve(Attempt.passed(webdriver));
97+
}).catch((e) => {
98+
driverDeferred.reject(Attempt.failed(e));
99+
});
100+
101+
shutdownServices.push(service.shutdown);
102+
103+
const { location, webdriver } = await driverPromise;
104+
console.log('Started webdriver session: ', webdriver.sessionId);
93105

94106
const cancelEverything = Lifecycle.cancel(webdriver, shutdown(shutdownServices), settings.gruntDone);
95107
process.on('SIGINT', cancelEverything);
@@ -119,13 +131,13 @@ export const go = (bedrockAutoSettings: BedrockAutoSettings): void => {
119131
} catch (e) {
120132
return Lifecycle.error(e as any, webdriver, shutdown(shutdownServices), settings.gruntDone, settings.delayExit);
121133
}
122-
}).catch(async (err) => {
134+
} catch(err) {
123135
// Chalk does not use a formatter. Using node's built-in to expand Objects, etc.
124136
console.error(chalk.red('Error creating webdriver', format(err)));
125137
// Shutdown tunnels in case webdriver fails
126138
await shutdown(shutdownServices)(true);
127-
Lifecycle.exit(settings.gruntDone, ExitCodes.failures.unexpected);
128-
});
139+
return Lifecycle.exit(settings.gruntDone, ExitCodes.failures.unexpected);
140+
}
129141
};
130142

131143
export const mode = 'forAuto';

modules/server/src/main/ts/BedrockCli.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ import * as Util from 'util';
88
Util.inspect.defaultOptions.depth = null;
99

1010
type Program = {
11-
go: <T extends BedrockSettings>(settings: T, directories: { current: string; bin: string }) => void;
11+
go: <T extends BedrockSettings>(settings: T, directories: { current: string; bin: string }) => Promise<void>;
1212
mode: 'forAuto' | 'forManual';
1313
}
1414

15-
export const run = (program: Program, directories: { current: string; bin: string }): void => {
15+
export const run = async (program: Program, directories: { current: string; bin: string }): Promise<void> => {
1616
if (Clis[program.mode] === undefined) {
1717
throw new Error('Bedrock mode not known: ' + program.mode);
1818
}
1919

2020
const maybeSettings: Attempt<CliError, BedrockSettings> = Clis[program.mode](directories);
21-
Attempt.cata(maybeSettings, Clis.logAndExit, (settings) => {
22-
program.go(settings, directories);
21+
await Attempt.cata(maybeSettings, Clis.logAndExit, async (settings) => {
22+
await program.go(settings, directories);
2323
});
2424
};

0 commit comments

Comments
 (0)