Fixed lazy CLI helper imports for tunnel flows Restored update command version detection Made web test and dead-code commands run reliably
217 lines
6.6 KiB
JavaScript
217 lines
6.6 KiB
JavaScript
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,
|
|
};
|