From a1a104533e60210d120e5d41bea5ca587768ada7 Mon Sep 17 00:00:00 2001 From: Mike Han Date: Thu, 10 Oct 2024 09:57:05 -0600 Subject: [PATCH 01/11] Add tunnel api --- src/api.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/api.ts b/src/api.ts index e18868b..ce6d2bd 100644 --- a/src/api.ts +++ b/src/api.ts @@ -29,6 +29,13 @@ export interface Platform { os: string; } +export interface Tunnel { + id: string; + owner: string; + status: string; + tunnel_identifier: string; +} + export async function getPlatforms(params: { username: string; accessKey: string; @@ -46,3 +53,29 @@ export async function getPlatforms(params: { return resp; } + +export async function getTunnels(params: { + username: string; + accessKey: string; + region: string; + filter: string; +}) { + const { username, accessKey, region, filter } = params; + // TODO: Error handling? + const resp = await axios.get<{ [key: string]: Tunnel[] }>( + `https://api.${region}.saucelabs.com/rest/v1/${username}/tunnels`, + { + auth: { + username, + password: accessKey, + }, + params: { + full: true, + all: true, + filter: filter !== '' ? filter : undefined, + }, + }, + ); + + return resp.data; +} From c9acb2eb6a066c1c7a954d51968c45f7d5d10ace Mon Sep 17 00:00:00 2001 From: Mike Han Date: Thu, 10 Oct 2024 09:59:48 -0600 Subject: [PATCH 02/11] feat: check for tunnel readiness --- src/index.ts | 15 +++++++++++++++ src/tunnel.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/tunnel.ts diff --git a/src/index.ts b/src/index.ts index 55b61d1..fb3a414 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { import { getPlatforms } from './api'; import { rcompareOses, rcompareVersions } from './sort'; import { isDevice } from './device'; +import { isTunnelRunning } from './tunnel'; type Browser = string; type Version = string; @@ -49,6 +50,7 @@ module.exports = { const tags = (process.env.SAUCE_TAGS || '').split(','); const region = process.env.SAUCE_REGION || 'us-west-1'; const jobName = process.env.SAUCE_JOB_NAME; + const tunnelWait = Number(process.env.SAUCE_TUNNEL_WAIT_TIME) || 60 * 1000; if (!username || !accessKey) { throw new AuthError(); @@ -64,6 +66,19 @@ module.exports = { throw new InvalidRegionError(); } + console.log(`Waiting ${tunnelWait}ms for tunnel ${tunnelName} to be ready`); + const validTunnel = await isTunnelRunning( + username, + accessKey, + region, + tunnelName, + tunnelWait, + ); + if (!validTunnel) { + throw new Error('Waited but tunnel did not become available'); + } + console.log('Tunnel is ready'); + sauceDriver = new SauceDriver( username, accessKey, diff --git a/src/tunnel.ts b/src/tunnel.ts new file mode 100644 index 0000000..2dccfa4 --- /dev/null +++ b/src/tunnel.ts @@ -0,0 +1,45 @@ +import { getTunnels } from './api'; + +async function timeout(delay: number) { + return new Promise((resolve) => setTimeout(resolve, delay)); +} + +export async function isTunnelRunning( + username: string, + accessKey: string, + region: string, + tunnelName: string, + wait: number, +): Promise { + return await Promise.race([ + (async function (): Promise { + // eslint-disable-next-line no-constant-condition + while (true) { + const allTunnels = await getTunnels({ + username, + accessKey, + region, + filter: tunnelName, + }); + for (const owner in allTunnels) { + const tunnels = allTunnels[owner]; + if ( + tunnels.some( + (t) => + t.owner === username && + (t.tunnel_identifier === tunnelName || t.id === tunnelName) && + t.status === 'running', + ) + ) { + return true; + } + } + await timeout(1000); + } + })(), + (async function (): Promise { + await timeout(wait); + return false; + })(), + ]); +} From 9114e3d8c3ec567d07a9a48b6225d6692a548682 Mon Sep 17 00:00:00 2001 From: Mike Han Date: Thu, 10 Oct 2024 10:15:52 -0600 Subject: [PATCH 03/11] Encapsulate error --- src/errors.ts | 7 +++++++ src/index.ts | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/errors.ts b/src/errors.ts index 62f46ac..84ff303 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -18,6 +18,13 @@ export class TunnelNameError extends Error { } } +export class TunnelNotReadyError extends Error { + constructor() { + super('Timed out waiting for a tunnel to be ready.'); + this.name = 'TunnelNotReadyError'; + } +} + export class CreateSessionError extends Error { constructor() { super('Failed to run test on Sauce Labs: no session id returned'); diff --git a/src/index.ts b/src/index.ts index fb3a414..f3ccabc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { AuthError, InvalidRegionError, TunnelNameError, + TunnelNotReadyError, WindowSizeRangeError, } from './errors'; import { getPlatforms } from './api'; @@ -46,11 +47,11 @@ module.exports = { const username = process.env.SAUCE_USERNAME; const accessKey = process.env.SAUCE_ACCESS_KEY; const tunnelName = process.env.SAUCE_TUNNEL_NAME; + const tunnelWait = Number(process.env.SAUCE_TUNNEL_WAIT_MS) || 60 * 1000; const build = process.env.SAUCE_BUILD; const tags = (process.env.SAUCE_TAGS || '').split(','); const region = process.env.SAUCE_REGION || 'us-west-1'; const jobName = process.env.SAUCE_JOB_NAME; - const tunnelWait = Number(process.env.SAUCE_TUNNEL_WAIT_TIME) || 60 * 1000; if (!username || !accessKey) { throw new AuthError(); @@ -75,7 +76,7 @@ module.exports = { tunnelWait, ); if (!validTunnel) { - throw new Error('Waited but tunnel did not become available'); + throw new TunnelNotReadyError(); } console.log('Tunnel is ready'); From fa88da5c9f58e86ea36bffcfc2f6175454be1f94 Mon Sep 17 00:00:00 2001 From: Mike Han Date: Thu, 10 Oct 2024 11:05:40 -0600 Subject: [PATCH 04/11] Api error handling --- src/api.ts | 55 +++++++++++++++++++++++++++++++++++---------------- src/tunnel.ts | 14 +++++++++---- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/api.ts b/src/api.ts index ce6d2bd..cfd321a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios, { isAxiosError } from 'axios'; export interface Platform { /** @@ -36,6 +36,10 @@ export interface Tunnel { tunnel_identifier: string; } +export type ApiResult = + | { kind: 'ok'; data: T } + | { kind: 'err'; data: E }; + export async function getPlatforms(params: { username: string; accessKey: string; @@ -59,23 +63,40 @@ export async function getTunnels(params: { accessKey: string; region: string; filter: string; -}) { +}): Promise> { const { username, accessKey, region, filter } = params; - // TODO: Error handling? - const resp = await axios.get<{ [key: string]: Tunnel[] }>( - `https://api.${region}.saucelabs.com/rest/v1/${username}/tunnels`, - { - auth: { - username, - password: accessKey, - }, - params: { - full: true, - all: true, - filter: filter !== '' ? filter : undefined, + try { + const resp = await axios.get<{ [key: string]: Tunnel[] }>( + `https://api.${region}.saucelabs.com/rest/v1/${username}/tunnels`, + { + auth: { + username, + password: accessKey, + }, + params: { + full: true, + all: true, + filter: filter !== '' ? filter : undefined, + }, }, - }, - ); + ); - return resp.data; + return { + kind: 'ok', + data: resp.data, + }; + } catch (e) { + if (isAxiosError(e)) { + return { + kind: 'err', + data: new Error( + `unexpected response (${e.status}) fetching tunnels: ${e.message}`, + ), + }; + } + return { + kind: 'err', + data: new Error(`unknown error fetching tunnel status: ${e}`), + }; + } } diff --git a/src/tunnel.ts b/src/tunnel.ts index 2dccfa4..b91a958 100644 --- a/src/tunnel.ts +++ b/src/tunnel.ts @@ -1,6 +1,6 @@ import { getTunnels } from './api'; -async function timeout(delay: number) { +async function sleep(delay: number) { return new Promise((resolve) => setTimeout(resolve, delay)); } @@ -15,12 +15,18 @@ export async function isTunnelRunning( (async function (): Promise { // eslint-disable-next-line no-constant-condition while (true) { - const allTunnels = await getTunnels({ + const result = await getTunnels({ username, accessKey, region, filter: tunnelName, }); + if (result.kind !== 'ok') { + await sleep(1000); + continue; + } + + const allTunnels = result.data; for (const owner in allTunnels) { const tunnels = allTunnels[owner]; if ( @@ -34,11 +40,11 @@ export async function isTunnelRunning( return true; } } - await timeout(1000); + await sleep(1000); } })(), (async function (): Promise { - await timeout(wait); + await sleep(wait); return false; })(), ]); From d09c73faa2c0b1d905fc487cc49c9e65041f5b58 Mon Sep 17 00:00:00 2001 From: Mike Han Date: Thu, 10 Oct 2024 11:28:23 -0600 Subject: [PATCH 05/11] User facing time in seconds not milliseconds --- README.md | 2 ++ src/index.ts | 6 ++++-- src/tunnel.ts | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8781d6a..a9245c4 100644 --- a/README.md +++ b/README.md @@ -81,3 +81,5 @@ Optional environment variables: - `SAUCE_TAGS` - A comma separated list of tags to apply to all jobs. - `SAUCE_REGION` - The Sauce Labs region. Valid values are `us-west-1` (default) or `eu-central-1`. - `SAUCE_SCREEN_RESOLUTION` - The desktop browser screen resolution (not applicable to mobile). The format is `1920x1080`. +- `SAUCE_TUNNEL_WAIT_SEC` - The amount of time to wait, in seconds, for + the tunnel defined by `SAUCE_TUNNEL_NAME` to be ready. Default is "60". diff --git a/src/index.ts b/src/index.ts index f3ccabc..7b9963a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,7 +47,7 @@ module.exports = { const username = process.env.SAUCE_USERNAME; const accessKey = process.env.SAUCE_ACCESS_KEY; const tunnelName = process.env.SAUCE_TUNNEL_NAME; - const tunnelWait = Number(process.env.SAUCE_TUNNEL_WAIT_MS) || 60 * 1000; + const tunnelWait = Number(process.env.SAUCE_TUNNEL_WAIT_SEC) || 60; const build = process.env.SAUCE_BUILD; const tags = (process.env.SAUCE_TAGS || '').split(','); const region = process.env.SAUCE_REGION || 'us-west-1'; @@ -67,7 +67,9 @@ module.exports = { throw new InvalidRegionError(); } - console.log(`Waiting ${tunnelWait}ms for tunnel ${tunnelName} to be ready`); + console.log( + `Waiting up to ${tunnelWait}s for tunnel "${tunnelName}" to be ready`, + ); const validTunnel = await isTunnelRunning( username, accessKey, diff --git a/src/tunnel.ts b/src/tunnel.ts index b91a958..a43dd44 100644 --- a/src/tunnel.ts +++ b/src/tunnel.ts @@ -44,7 +44,7 @@ export async function isTunnelRunning( } })(), (async function (): Promise { - await sleep(wait); + await sleep(wait * 1000); return false; })(), ]); From eb4f585688d48d4f5430d2cbdb7f5401788f77e3 Mon Sep 17 00:00:00 2001 From: Mike Han Date: Thu, 10 Oct 2024 11:35:19 -0600 Subject: [PATCH 06/11] Consistent error messages --- src/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api.ts b/src/api.ts index cfd321a..219ab8d 100644 --- a/src/api.ts +++ b/src/api.ts @@ -96,7 +96,7 @@ export async function getTunnels(params: { } return { kind: 'err', - data: new Error(`unknown error fetching tunnel status: ${e}`), + data: new Error(`unknown error fetching tunnels: ${e}`), }; } } From b4609894e9dcb5e445b3977b88c536cb191e324e Mon Sep 17 00:00:00 2001 From: Mike Han Date: Thu, 10 Oct 2024 11:39:35 -0600 Subject: [PATCH 07/11] Better naming --- src/api.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api.ts b/src/api.ts index 219ab8d..71f4a3c 100644 --- a/src/api.ts +++ b/src/api.ts @@ -38,7 +38,7 @@ export interface Tunnel { export type ApiResult = | { kind: 'ok'; data: T } - | { kind: 'err'; data: E }; + | { kind: 'err'; error: E }; export async function getPlatforms(params: { username: string; @@ -89,14 +89,14 @@ export async function getTunnels(params: { if (isAxiosError(e)) { return { kind: 'err', - data: new Error( + error: new Error( `unexpected response (${e.status}) fetching tunnels: ${e.message}`, ), }; } return { kind: 'err', - data: new Error(`unknown error fetching tunnels: ${e}`), + error: new Error(`unknown error fetching tunnels: ${e}`), }; } } From c9acf3611e3b229a6a2646a3a7813cb29813553a Mon Sep 17 00:00:00 2001 From: Mike Han Date: Thu, 10 Oct 2024 11:42:21 -0600 Subject: [PATCH 08/11] Sleep a bit longer if tunnel api call errored --- src/tunnel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tunnel.ts b/src/tunnel.ts index a43dd44..75dae99 100644 --- a/src/tunnel.ts +++ b/src/tunnel.ts @@ -22,7 +22,7 @@ export async function isTunnelRunning( filter: tunnelName, }); if (result.kind !== 'ok') { - await sleep(1000); + await sleep(2000); continue; } From f568226cc2a412e58c49d3336f045d5ce5268656 Mon Sep 17 00:00:00 2001 From: Mike Han Date: Thu, 10 Oct 2024 12:52:27 -0600 Subject: [PATCH 09/11] Don't hammer the api if auth is failing --- src/api.ts | 8 +++++++- src/index.ts | 8 +++++--- src/tunnel.ts | 15 +++++++++------ 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/api.ts b/src/api.ts index 71f4a3c..4373d9e 100644 --- a/src/api.ts +++ b/src/api.ts @@ -38,7 +38,8 @@ export interface Tunnel { export type ApiResult = | { kind: 'ok'; data: T } - | { kind: 'err'; error: E }; + | { kind: 'err'; error: E } + | { kind: 'unauthorized' }; export async function getPlatforms(params: { username: string; @@ -87,6 +88,11 @@ export async function getTunnels(params: { }; } catch (e) { if (isAxiosError(e)) { + if (e.response?.status === 401) { + return { + kind: 'unauthorized', + }; + } return { kind: 'err', error: new Error( diff --git a/src/index.ts b/src/index.ts index 7b9963a..1871b3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ import { import { getPlatforms } from './api'; import { rcompareOses, rcompareVersions } from './sort'; import { isDevice } from './device'; -import { isTunnelRunning } from './tunnel'; +import { waitForTunnel } from './tunnel'; type Browser = string; type Version = string; @@ -70,15 +70,17 @@ module.exports = { console.log( `Waiting up to ${tunnelWait}s for tunnel "${tunnelName}" to be ready`, ); - const validTunnel = await isTunnelRunning( + const tunnelStatus = await waitForTunnel( username, accessKey, region, tunnelName, tunnelWait, ); - if (!validTunnel) { + if (tunnelStatus === 'notready') { throw new TunnelNotReadyError(); + } else if (tunnelStatus === 'unauthorized') { + throw new AuthError(); } console.log('Tunnel is ready'); diff --git a/src/tunnel.ts b/src/tunnel.ts index 75dae99..32c29b7 100644 --- a/src/tunnel.ts +++ b/src/tunnel.ts @@ -4,15 +4,15 @@ async function sleep(delay: number) { return new Promise((resolve) => setTimeout(resolve, delay)); } -export async function isTunnelRunning( +export async function waitForTunnel( username: string, accessKey: string, region: string, tunnelName: string, wait: number, -): Promise { +): Promise<'ok' | 'notready' | 'unauthorized'> { return await Promise.race([ - (async function (): Promise { + (async function (): Promise<'ok' | 'unauthorized'> { // eslint-disable-next-line no-constant-condition while (true) { const result = await getTunnels({ @@ -21,6 +21,9 @@ export async function isTunnelRunning( region, filter: tunnelName, }); + if (result.kind === 'unauthorized') { + return 'unauthorized'; + } if (result.kind !== 'ok') { await sleep(2000); continue; @@ -37,15 +40,15 @@ export async function isTunnelRunning( t.status === 'running', ) ) { - return true; + return 'ok'; } } await sleep(1000); } })(), - (async function (): Promise { + (async function (): Promise<'notready'> { await sleep(wait * 1000); - return false; + return 'notready'; })(), ]); } From 19bf3801df403f947b6c490dbc628ef4d8eb5a2b Mon Sep 17 00:00:00 2001 From: Mike Han Date: Thu, 10 Oct 2024 12:55:16 -0600 Subject: [PATCH 10/11] copy editing --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 1871b3a..ea18d8f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,7 +68,7 @@ module.exports = { } console.log( - `Waiting up to ${tunnelWait}s for tunnel "${tunnelName}" to be ready`, + `Waiting up to ${tunnelWait}s for tunnel "${tunnelName}" to be ready...`, ); const tunnelStatus = await waitForTunnel( username, From 7b5ecbb7db56b3ed9bd1484b57935cde4d0915af Mon Sep 17 00:00:00 2001 From: Mike Han Date: Thu, 10 Oct 2024 12:56:29 -0600 Subject: [PATCH 11/11] default to 30s --- README.md | 2 +- src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a9245c4..6c35a80 100644 --- a/README.md +++ b/README.md @@ -82,4 +82,4 @@ Optional environment variables: - `SAUCE_REGION` - The Sauce Labs region. Valid values are `us-west-1` (default) or `eu-central-1`. - `SAUCE_SCREEN_RESOLUTION` - The desktop browser screen resolution (not applicable to mobile). The format is `1920x1080`. - `SAUCE_TUNNEL_WAIT_SEC` - The amount of time to wait, in seconds, for - the tunnel defined by `SAUCE_TUNNEL_NAME` to be ready. Default is "60". + the tunnel defined by `SAUCE_TUNNEL_NAME` to be ready. Default is "30". diff --git a/src/index.ts b/src/index.ts index ea18d8f..ef5e3ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,7 +47,7 @@ module.exports = { const username = process.env.SAUCE_USERNAME; const accessKey = process.env.SAUCE_ACCESS_KEY; const tunnelName = process.env.SAUCE_TUNNEL_NAME; - const tunnelWait = Number(process.env.SAUCE_TUNNEL_WAIT_SEC) || 60; + const tunnelWait = Number(process.env.SAUCE_TUNNEL_WAIT_SEC) || 30; const build = process.env.SAUCE_BUILD; const tags = (process.env.SAUCE_TAGS || '').split(','); const region = process.env.SAUCE_REGION || 'us-west-1';