fix(web-probe): recover observe startup after transport timeout
This commit is contained in:
@@ -25,6 +25,10 @@ const maxSamples = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_SAMPLES,
|
||||
const observerRefreshIntervalMs = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_OBSERVER_REFRESH_INTERVAL_MS, 180000);
|
||||
const viewport = parseViewport(process.env.UNIDESK_WEB_OBSERVE_VIEWPORT || "1440x900");
|
||||
const browserProxyMode = parseBrowserProxyMode(process.env.UNIDESK_WEB_OBSERVE_BROWSER_PROXY_MODE || "auto");
|
||||
const authLoginMaxAttempts = boundedInteger(process.env.UNIDESK_WEB_OBSERVE_AUTH_LOGIN_MAX_ATTEMPTS, 6, 1, 20);
|
||||
const authLoginRequestTimeoutMs = boundedInteger(process.env.UNIDESK_WEB_OBSERVE_AUTH_LOGIN_REQUEST_TIMEOUT_MS, 30000, 1000, 120000);
|
||||
const authLoginInitialDelayMs = boundedInteger(process.env.UNIDESK_WEB_OBSERVE_AUTH_LOGIN_INITIAL_DELAY_MS, 500, 0, 60000);
|
||||
const authLoginMaxDelayMs = boundedInteger(process.env.UNIDESK_WEB_OBSERVE_AUTH_LOGIN_MAX_DELAY_MS, 10000, 0, 120000);
|
||||
const alertThresholds = parseAlertThresholds(process.env.UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON);
|
||||
const projectManagement = parseProjectManagementConfig(process.env.UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON);
|
||||
const playwrightProxy = proxyConfigFromEnv(baseUrl);
|
||||
@@ -493,11 +497,9 @@ async function runControlCommand(command, fn) {
|
||||
async function authenticate(browserContext) {
|
||||
const loginUrl = new URL("/auth/login", baseUrl).toString();
|
||||
const attempts = [];
|
||||
const maxAttempts = 5;
|
||||
const initialDelayMs = 250;
|
||||
const maxDelayMs = 5000;
|
||||
const maxAttempts = authLoginMaxAttempts;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
const retryDelayMs = attempt < maxAttempts ? Math.min(maxDelayMs, initialDelayMs * (2 ** (attempt - 1))) : 0;
|
||||
const retryDelayMs = authRetryDelayMs(attempt, maxAttempts);
|
||||
const retryLabel = attempt + "/" + maxAttempts;
|
||||
await writeHeartbeat({ status: terminalStatus, auth: { phase: "api-login", retryAttempt: attempt, retryMaxAttempts: maxAttempts, lastRetryLabel: retryLabel, retryDelayMs: 0, retryExhausted: false, valuesRedacted: true } }).catch(() => {});
|
||||
try {
|
||||
@@ -510,6 +512,7 @@ async function authenticate(browserContext) {
|
||||
retryMaxAttempts: maxAttempts,
|
||||
retryLabel,
|
||||
retryDelayMs: retryable && attempt < maxAttempts ? retryDelayMs : 0,
|
||||
requestTimeoutMs: authLoginRequestTimeoutMs,
|
||||
method: "api",
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
@@ -547,6 +550,7 @@ async function authenticate(browserContext) {
|
||||
retryMaxAttempts: maxAttempts,
|
||||
retryLabel,
|
||||
retryDelayMs: retryable && attempt < maxAttempts ? retryDelayMs : 0,
|
||||
requestTimeoutMs: authLoginRequestTimeoutMs,
|
||||
method: "api",
|
||||
status: 0,
|
||||
statusText: "request-error",
|
||||
@@ -593,7 +597,7 @@ async function pageAuthLogin(browserContext, loginUrl, credential = { username,
|
||||
const response = await browserContext.request.post(loginUrl, {
|
||||
data: { username: credential.username, password: credential.password },
|
||||
headers: { accept: "application/json", "content-type": "application/json" },
|
||||
timeout: 12000,
|
||||
timeout: authLoginRequestTimeoutMs,
|
||||
});
|
||||
await response.text().catch(() => "");
|
||||
return {
|
||||
@@ -608,11 +612,79 @@ async function loginAccount(command) {
|
||||
const credential = credentialForAccount(accountId);
|
||||
const loginUrl = new URL("/auth/login", baseUrl).toString();
|
||||
const before = await accountSessionSnapshot();
|
||||
const response = await pageAuthLogin(context, loginUrl, credential);
|
||||
const cookieState = await readAuthCookieState(context);
|
||||
const attempts = [];
|
||||
let response = null;
|
||||
let cookieState = null;
|
||||
const maxAttempts = authLoginMaxAttempts;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
const retryDelayMs = authRetryDelayMs(attempt, maxAttempts);
|
||||
const retryLabel = attempt + "/" + maxAttempts;
|
||||
try {
|
||||
response = await pageAuthLogin(context, loginUrl, credential);
|
||||
cookieState = await readAuthCookieState(context);
|
||||
const retryable = isRetryableAuthStatus(response.status);
|
||||
attempts.push({
|
||||
attempt,
|
||||
retryAttempt: attempt,
|
||||
retryMaxAttempts: maxAttempts,
|
||||
retryLabel,
|
||||
retryDelayMs: retryable && attempt < maxAttempts ? retryDelayMs : 0,
|
||||
requestTimeoutMs: authLoginRequestTimeoutMs,
|
||||
method: "api",
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
retryable,
|
||||
cookiePresent: cookieState.cookiePresent,
|
||||
cookieNames: cookieState.cookieNames,
|
||||
credentialSource: credential.source,
|
||||
valuesRedacted: true,
|
||||
});
|
||||
if (response.ok && cookieState.cookiePresent) break;
|
||||
if (!retryable) break;
|
||||
} catch (error) {
|
||||
const retryable = isRetryableAuthError(error);
|
||||
attempts.push({
|
||||
attempt,
|
||||
retryAttempt: attempt,
|
||||
retryMaxAttempts: maxAttempts,
|
||||
retryLabel,
|
||||
retryDelayMs: retryable && attempt < maxAttempts ? retryDelayMs : 0,
|
||||
requestTimeoutMs: authLoginRequestTimeoutMs,
|
||||
method: "api",
|
||||
status: 0,
|
||||
statusText: "request-error",
|
||||
retryable,
|
||||
error: error && error.message ? truncate(error.message, 500) : truncate(String(error), 500),
|
||||
cookiePresent: false,
|
||||
cookieNames: [],
|
||||
credentialSource: credential.source,
|
||||
valuesRedacted: true,
|
||||
});
|
||||
response = { ok: false, status: 0, statusText: "request-error" };
|
||||
cookieState = await readAuthCookieState(context).catch(() => ({ cookiePresent: false, cookieNames: [] }));
|
||||
if (!retryable) break;
|
||||
}
|
||||
if (attempt < maxAttempts && attempts[attempts.length - 1]?.retryable === true) await sleep(retryDelayMs);
|
||||
}
|
||||
response = response ?? { ok: false, status: 0, statusText: "api-login-failed" };
|
||||
cookieState = cookieState ?? await readAuthCookieState(context);
|
||||
if (!response.ok || !cookieState.cookiePresent) {
|
||||
const error = new Error("loginAccount failed for accountId=" + accountId + " status=" + response.status + " " + (response.statusText || ""));
|
||||
error.details = { accountId, status: response.status, statusText: response.statusText, cookiePresent: cookieState.cookiePresent, credentialSource: credential.source, valuesRedacted: true };
|
||||
const retryable = attempts.some((item) => item && item.retryable === true);
|
||||
error.details = {
|
||||
accountId,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
cookiePresent: cookieState.cookiePresent,
|
||||
credentialSource: credential.source,
|
||||
attempts,
|
||||
retryCount: Math.max(0, attempts.length - 1),
|
||||
retryMaxAttempts: maxAttempts,
|
||||
lastRetryLabel: attempts[attempts.length - 1]?.retryLabel || null,
|
||||
retryExhausted: retryable && attempts.length >= maxAttempts,
|
||||
retryable,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
const target = isWorkbenchPathname(safeUrlPath(currentPageUrl()) || "") ? safeUrlPath(currentPageUrl()) : targetPath;
|
||||
@@ -786,6 +858,10 @@ function isRetryableAuthError(error) {
|
||||
return /AbortError|EAI_AGAIN|ETIMEDOUT|ECONNRESET|ECONNREFUSED|ECONNABORTED|socket hang up|ERR_NETWORK_CHANGED|fetch failed|failed to fetch|network|timeout|aborted/iu.test(message);
|
||||
}
|
||||
|
||||
function authRetryDelayMs(attempt, maxAttempts) {
|
||||
return attempt < maxAttempts ? Math.min(authLoginMaxDelayMs, authLoginInitialDelayMs * (2 ** (attempt - 1))) : 0;
|
||||
}
|
||||
|
||||
function authFailureMessage(failure) {
|
||||
const last = Array.isArray(failure.attempts) && failure.attempts.length > 0 ? failure.attempts[failure.attempts.length - 1] : null;
|
||||
const retry = failure.lastRetryLabel ? " retry=" + failure.lastRetryLabel : "";
|
||||
@@ -3767,6 +3843,14 @@ function positiveInteger(value, fallback) {
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : fallback;
|
||||
}
|
||||
|
||||
function boundedInteger(value, fallback, min, max) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
const integer = Math.floor(parsed);
|
||||
if (integer < min || integer > max) return fallback;
|
||||
return integer;
|
||||
}
|
||||
|
||||
function positiveNumber(value, fallback) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||
|
||||
Reference in New Issue
Block a user