Refactor web CLI into focused modules (#1837)
This commit is contained in:
committed by
GitHub
parent
00821700de
commit
45df19c3b2
@@ -0,0 +1,216 @@
|
||||
import { buildLocalUrl } from './cli-network.js';
|
||||
import { getInstanceFilePath, readInstanceOptions } from './cli-process.js';
|
||||
|
||||
const UI_SESSION_COOKIE_NAME = 'oc_ui_session';
|
||||
|
||||
function extractUiSessionCookie(response) {
|
||||
const setCookie = response?.headers?.get?.('set-cookie');
|
||||
if (typeof setCookie !== 'string' || setCookie.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const match = setCookie.match(new RegExp(`(?:^|,\\s*)(${UI_SESSION_COOKIE_NAME}=[^;]+)`));
|
||||
return match?.[1] || null;
|
||||
}
|
||||
|
||||
async function resolveUiPasswordForPort(port, options = {}) {
|
||||
if (typeof options.uiPassword === 'string' && options.uiPassword.trim().length > 0) {
|
||||
return options.uiPassword;
|
||||
}
|
||||
const instanceOptions = readInstanceOptions(await getInstanceFilePath(port));
|
||||
return typeof instanceOptions?.uiPassword === 'string' && instanceOptions.uiPassword.trim().length > 0
|
||||
? instanceOptions.uiPassword
|
||||
: null;
|
||||
}
|
||||
|
||||
async function createUiSessionCookie(port, password, timeoutMs) {
|
||||
if (typeof password !== 'string' || password.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetch(buildLocalUrl(port, '/auth/session'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ password }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
return extractUiSessionCookie(response);
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function requestServerShutdown(port, hostOverride) {
|
||||
if (!Number.isFinite(port) || port <= 0) return false;
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 1500);
|
||||
try {
|
||||
const resp = await fetch(buildLocalUrl(port, '/api/system/shutdown', hostOverride), {
|
||||
method: 'POST',
|
||||
signal: controller.signal,
|
||||
});
|
||||
return resp.ok;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function requestJson(port, endpoint, options = {}) {
|
||||
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
|
||||
? Math.trunc(options.timeoutMs)
|
||||
: 4000;
|
||||
const fetchOptions = { ...options };
|
||||
delete fetchOptions.timeoutMs;
|
||||
delete fetchOptions.uiPassword;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const requestUrl = buildLocalUrl(port, endpoint);
|
||||
const requestHeaders = {
|
||||
Accept: 'application/json',
|
||||
...(fetchOptions.body ? { 'Content-Type': 'application/json' } : {}),
|
||||
...(fetchOptions.headers || {}),
|
||||
};
|
||||
const response = await fetch(requestUrl, {
|
||||
...fetchOptions,
|
||||
headers: requestHeaders,
|
||||
signal: controller.signal,
|
||||
});
|
||||
const body = await response.json().catch(() => null);
|
||||
if (response.status === 401 && body?.error === 'UI authentication required') {
|
||||
const uiPassword = await resolveUiPasswordForPort(port, options);
|
||||
const cookie = await createUiSessionCookie(port, uiPassword, timeoutMs);
|
||||
if (cookie) {
|
||||
const retryResponse = await fetch(requestUrl, {
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
...requestHeaders,
|
||||
Cookie: cookie,
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
const retryBody = await retryResponse.json().catch(() => null);
|
||||
return { response: retryResponse, body: retryBody };
|
||||
}
|
||||
}
|
||||
return { response, body };
|
||||
} catch (error) {
|
||||
if (error && (error.name === 'AbortError' || error.code === 'ABORT_ERR')) {
|
||||
throw new Error(`Request to ${endpoint} timed out after ${timeoutMs}ms.`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function isServerHealthReady(port, timeoutMs = 1000) {
|
||||
if (!Number.isFinite(port) || port <= 0) {
|
||||
return false;
|
||||
}
|
||||
const requestTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.trunc(timeoutMs) : 1000;
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), requestTimeout);
|
||||
try {
|
||||
const response = await fetch(buildLocalUrl(port, '/health'), {
|
||||
headers: { Accept: 'text/plain' },
|
||||
signal: controller.signal,
|
||||
});
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForServerHealth(port, {
|
||||
timeoutMs = 60000,
|
||||
intervalMs = 250,
|
||||
onTick,
|
||||
} = {}) {
|
||||
const start = Date.now();
|
||||
const deadline = start + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const elapsedMs = Date.now() - start;
|
||||
if (typeof onTick === 'function') {
|
||||
onTick({ elapsedMs, timeoutMs });
|
||||
}
|
||||
if (await isServerHealthReady(port, Math.min(1000, intervalMs * 2))) {
|
||||
if (typeof onTick === 'function') {
|
||||
onTick({ elapsedMs: Math.min(Date.now() - start, timeoutMs), timeoutMs, complete: true });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||
}
|
||||
if (typeof onTick === 'function') {
|
||||
onTick({ elapsedMs: timeoutMs, timeoutMs, timedOut: true });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
async function fetchTunnelProvidersFromPort(port, fetchImpl = globalThis.fetch) {
|
||||
if (!Number.isFinite(port) || port <= 0 || typeof fetchImpl !== 'function') {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const response = await fetchImpl(buildLocalUrl(port, '/api/openchamber/tunnel/providers'));
|
||||
if (!response.ok) return null;
|
||||
const body = await response.json().catch(() => null);
|
||||
if (!body || !Array.isArray(body.providers)) return null;
|
||||
return body.providers;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSystemInfoFromPort(port, fetchImpl = globalThis.fetch, hostOverride) {
|
||||
if (!Number.isFinite(port) || port <= 0 || typeof fetchImpl !== 'function') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 1500);
|
||||
try {
|
||||
const response = await fetchImpl(buildLocalUrl(port, '/api/system/info', hostOverride), {
|
||||
headers: { Accept: 'application/json' },
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const body = await response.json().catch(() => null);
|
||||
if (!body || typeof body.runtime !== 'string') return null;
|
||||
|
||||
return {
|
||||
runtime: body.runtime,
|
||||
pid: Number.isFinite(body.pid) ? body.pid : null,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
requestServerShutdown,
|
||||
requestJson,
|
||||
isServerHealthReady,
|
||||
waitForServerHealth,
|
||||
fetchTunnelProvidersFromPort,
|
||||
fetchSystemInfoFromPort,
|
||||
};
|
||||
Reference in New Issue
Block a user