Skip to content

Commit dee3643

Browse files
committed
Use sc api for healthcheck
1 parent cff8d0b commit dee3643

File tree

3 files changed

+120
-62
lines changed

3 files changed

+120
-62
lines changed

src/constants.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,6 @@ export const SC_PARAMS_TO_STRIP = [
329329
...SAUCE_CONNECT_CLI_PARAM_ALIASES,
330330
];
331331

332-
export const SC_READY_MESSAGE = 'Sauce Connect is up, you may start your tests';
333-
export const SC_FAILURE_MESSAGES = ['fatal error exiting'];
334-
export const SC_CLOSE_MESSAGE = 'tunnel was shutdown';
335332
export const SC_CLOSE_TIMEOUT = 5000;
333+
export const SC_READY_TIMEOUT = 30000;
334+
export const SC_HEALTHCHECK_TIMEOUT = 1000;

src/index.js

+20-59
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,12 @@ import {
3030
SYMBOL_ITERATOR,
3131
TO_STRING_TAG,
3232
SC_PARAMS_TO_STRIP,
33-
SC_READY_MESSAGE,
34-
SC_CLOSE_MESSAGE,
35-
SC_CLOSE_TIMEOUT,
3633
DEFAULT_SAUCE_CONNECT_VERSION,
37-
SC_FAILURE_MESSAGES,
3834
SC_BOOLEAN_CLI_PARAMS,
3935
DEFAULT_RUNNER_NAME,
4036
} from './constants';
4137
import SauceConnectLoader from './sauceConnectLoader';
38+
import {SauceConnectManager} from './sauceConnectManager';
4239

4340
export default class SauceLabs {
4441
constructor(options) {
@@ -259,6 +256,7 @@ export default class SauceLabs {
259256
![
260257
'_',
261258
'$0',
259+
'api-address',
262260
'metadata',
263261
'sc-version',
264262
'sc-upstream-proxy',
@@ -294,14 +292,17 @@ export default class SauceLabs {
294292
}
295293

296294
// Provide a default runner name. It's used for identifying the tunnel's initiation method.
297-
let metadata = argv.metadata || "";
298-
if (!metadata.includes("runner=")) {
295+
let metadata = argv.metadata || '';
296+
if (!metadata.includes('runner=')) {
299297
metadata = [metadata, `runner=${DEFAULT_RUNNER_NAME}`]
300298
.filter(Boolean)
301-
.join(',')
299+
.join(',');
302300
}
303301
args.push(`--metadata=${metadata}`);
304302

303+
const apiAddress = argv.apiAddress || ':8032';
304+
args.push(`--api-address=${apiAddress}`);
305+
305306
const region = argv.region || this.region;
306307
if (region) {
307308
const scRegion = getRegionSubDomain({region});
@@ -338,60 +339,20 @@ export default class SauceLabs {
338339
args.unshift('run');
339340
}
340341

342+
const logger = fromCLI
343+
? process.stdout.write.bind(process.stdout)
344+
: argv.logger;
341345
const cp = spawn(scLoader.path, args);
342-
return new Promise((resolve, reject) => {
343-
const close = () =>
344-
new Promise((resolveClose) => {
345-
process.kill(cp.pid, 'SIGINT');
346-
const timeout = setTimeout(resolveClose, SC_CLOSE_TIMEOUT);
347-
cp.stdout.on('data', (data) => {
348-
const output = data.toString();
349-
if (output.includes(SC_CLOSE_MESSAGE)) {
350-
clearTimeout(timeout);
351-
return resolveClose(returnObj);
352-
}
353-
});
354-
});
355-
const returnObj = {cp, close};
356-
357-
cp.stderr.on('data', (data) => {
358-
const output = data.toString();
359-
return reject(new Error(output));
360-
});
361-
cp.stdout.on('data', (data) => {
362-
const logger = fromCLI
363-
? process.stdout.write.bind(process.stdout)
364-
: argv.logger;
365-
const output = data.toString();
366-
/**
367-
* print to stdout if called via CLI
368-
*/
369-
if (typeof logger === 'function') {
370-
logger(output);
371-
}
372-
373-
/**
374-
* fail if SauceConnect could not establish a connection
375-
*/
376-
if (
377-
SC_FAILURE_MESSAGES.find((msg) =>
378-
escape(output).includes(escape(msg))
379-
)
380-
) {
381-
return reject(new Error(output));
382-
}
346+
const manager = new SauceConnectManager(cp, logger);
347+
process.on('SIGINT', () => manager.close());
383348

384-
/**
385-
* continue if connection was established
386-
*/
387-
if (output.includes(SC_READY_MESSAGE)) {
388-
return resolve(returnObj);
389-
}
390-
});
391-
392-
process.on('SIGINT', close);
393-
return returnObj;
394-
});
349+
try {
350+
await manager.waitForReady(apiAddress);
351+
return {cp, close: () => manager.close()};
352+
} catch (err) {
353+
await manager.close();
354+
throw err;
355+
}
395356
}
396357

397358
/**

src/sauceConnectManager.js

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {
2+
SC_CLOSE_TIMEOUT,
3+
SC_HEALTHCHECK_TIMEOUT,
4+
SC_READY_TIMEOUT,
5+
} from './constants';
6+
7+
export class SauceConnectManager {
8+
constructor(cp, logger) {
9+
this.cp = cp;
10+
this.logger = logger;
11+
this._healthcheckInterval = null;
12+
this._readyTimeout = null;
13+
}
14+
15+
waitForReady(apiAddress) {
16+
apiAddress = this._parseApiAddress(apiAddress);
17+
18+
return new Promise((resolve, reject) => {
19+
this.cp.stderr.on('data', (data) => {
20+
const output = data.toString();
21+
return reject(new Error(output));
22+
});
23+
24+
this.cp.stdout.on('data', (data) => {
25+
const logger = this.logger;
26+
if (typeof logger === 'function') {
27+
logger(data.toString());
28+
}
29+
});
30+
31+
this._healthcheckInterval = setInterval(async () => {
32+
if (this.cp.exitCode !== null) {
33+
clearInterval(this._healthcheckInterval);
34+
clearTimeout(this._readyTimeout);
35+
return reject(new Error('Sauce Connect exited before ready'));
36+
}
37+
38+
this._callHealthcheckEndpoint(apiAddress)
39+
.then(() => clearInterval(this._healthcheckInterval))
40+
.then(() => clearTimeout(this._readyTimeout))
41+
.then(resolve)
42+
.catch(() => {});
43+
}, SC_HEALTHCHECK_TIMEOUT);
44+
45+
this._readyTimeout = setTimeout(() => {
46+
clearInterval(this._healthcheckInterval);
47+
reject(new Error('Timeout waiting for healthcheck endpoint'));
48+
}, SC_READY_TIMEOUT);
49+
});
50+
}
51+
52+
close() {
53+
this.logger('Terminating Sauce Connect...');
54+
55+
return new Promise((resolve) => {
56+
if (this.cp.exitCode !== null) {
57+
return resolve();
58+
}
59+
60+
const timeout = setTimeout(() => {
61+
this.logger('Forcefully terminating Sauce Connect...');
62+
killSilently(this.cp.pid, 'SIGKILL');
63+
resolve();
64+
}, SC_CLOSE_TIMEOUT);
65+
66+
this.cp.on('exit', () => {
67+
clearTimeout(timeout);
68+
resolve();
69+
});
70+
71+
clearInterval(this._healthcheckInterval);
72+
clearTimeout(this._readyTimeout);
73+
killSilently(this.cp.pid, 'SIGINT');
74+
});
75+
}
76+
77+
async _callHealthcheckEndpoint(apiAddress) {
78+
const response = await fetch(`${apiAddress}/readyz`, {
79+
signal: AbortSignal.timeout(SC_HEALTHCHECK_TIMEOUT),
80+
});
81+
if (response.status !== 200) {
82+
throw new Error(`response status code ${response.status} != 200`);
83+
}
84+
}
85+
86+
_parseApiAddress(value) {
87+
const [host, port] = value.split(':');
88+
return `http://${host || 'localhost'}:${port}`;
89+
}
90+
}
91+
92+
function killSilently(pid, signal) {
93+
try {
94+
process.kill(pid, signal);
95+
} catch (err) {
96+
// ignore
97+
}
98+
}

0 commit comments

Comments
 (0)