Skip to content

Commit 8707580

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

File tree

3 files changed

+113
-62
lines changed

3 files changed

+113
-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

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

0 commit comments

Comments
 (0)