mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-10 16:30:15 +00:00
672 lines
21 KiB
TypeScript
672 lines
21 KiB
TypeScript
/**
|
|
* Docker service management for tests.
|
|
*
|
|
* CouchDB start/stop/init is implemented directly using `docker` CLI commands
|
|
* and the Fetch API, so it works on any platform where Docker (Desktop) is
|
|
* available — including Windows — without needing bash.
|
|
*/
|
|
|
|
type DockerInvoker = {
|
|
bin: string;
|
|
prefix: string[];
|
|
label: string;
|
|
};
|
|
|
|
let dockerInvokerPromise: Promise<DockerInvoker> | null = null;
|
|
const DOCKER_TEE = Deno.env.get("LIVESYNC_DOCKER_TEE") === "1" || Deno.env.get("LIVESYNC_TEST_TEE") === "1";
|
|
const trackedContainers = new Set<string>();
|
|
const CLEANUP_SIGNALS: Deno.Signal[] = ["SIGINT", "SIGTERM"];
|
|
let signalCleanupHandlersInstalled = false;
|
|
let signalCleanupInProgress = false;
|
|
const signalCleanupHandlers = new Map<Deno.Signal, () => void>();
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Low-level docker wrapper
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function parseCommand(command: string): { bin: string; prefix: string[] } {
|
|
const parts = command.trim().split(/\s+/).filter(Boolean);
|
|
if (parts.length === 0) {
|
|
throw new Error("LIVESYNC_DOCKER_COMMAND is empty");
|
|
}
|
|
return { bin: parts[0], prefix: parts.slice(1) };
|
|
}
|
|
|
|
async function collectStream(
|
|
stream: ReadableStream<Uint8Array>,
|
|
teeTarget: ((chunk: Uint8Array) => void) | null
|
|
): Promise<Uint8Array> {
|
|
const reader = stream.getReader();
|
|
const chunks: Uint8Array[] = [];
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
if (!value) continue;
|
|
chunks.push(value);
|
|
if (teeTarget) {
|
|
teeTarget(value);
|
|
}
|
|
}
|
|
} finally {
|
|
reader.releaseLock();
|
|
}
|
|
|
|
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
const out = new Uint8Array(total);
|
|
let offset = 0;
|
|
for (const chunk of chunks) {
|
|
out.set(chunk, offset);
|
|
offset += chunk.length;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
async function runCommand(bin: string, args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
try {
|
|
const child = new Deno.Command(bin, {
|
|
args,
|
|
stdin: "null",
|
|
stdout: "piped",
|
|
stderr: "piped",
|
|
}).spawn();
|
|
const stdoutPromise = collectStream(child.stdout, DOCKER_TEE ? (chunk) => Deno.stdout.writeSync(chunk) : null);
|
|
const stderrPromise = collectStream(child.stderr, DOCKER_TEE ? (chunk) => Deno.stderr.writeSync(chunk) : null);
|
|
const [status, stdout, stderr] = await Promise.all([child.status, stdoutPromise, stderrPromise]);
|
|
const dec = new TextDecoder();
|
|
const result = {
|
|
code: status.code,
|
|
stdout: dec.decode(stdout),
|
|
stderr: dec.decode(stderr),
|
|
};
|
|
return result;
|
|
} catch (err) {
|
|
if (err instanceof Deno.errors.NotFound) {
|
|
return {
|
|
code: 127,
|
|
stdout: "",
|
|
stderr: `Command not found: ${bin}`,
|
|
};
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async function resolveDockerInvoker(): Promise<DockerInvoker> {
|
|
const custom = Deno.env.get("LIVESYNC_DOCKER_COMMAND")?.trim();
|
|
if (custom) {
|
|
const parsed = parseCommand(custom);
|
|
const runner: DockerInvoker = {
|
|
...parsed,
|
|
label: `custom(${custom})`,
|
|
};
|
|
|
|
// Validate custom command eagerly so misconfiguration fails fast.
|
|
const checkArgs = runner.prefix.length === 0 ? ["--version"] : [...runner.prefix, "docker", "--version"];
|
|
const check = await runCommand(runner.bin, checkArgs);
|
|
if (check.code !== 0) {
|
|
throw new Error(`LIVESYNC_DOCKER_COMMAND is not usable: ${custom}\n${check.stderr || check.stdout}`);
|
|
}
|
|
return runner;
|
|
}
|
|
|
|
const mode = (Deno.env.get("LIVESYNC_DOCKER_MODE") ?? "auto").toLowerCase();
|
|
const onWindows = Deno.build.os === "windows";
|
|
|
|
const native: DockerInvoker = { bin: "docker", prefix: [], label: "docker" };
|
|
const wsl: DockerInvoker = { bin: "wsl", prefix: [], label: "wsl docker" };
|
|
|
|
if (mode === "native") {
|
|
return native;
|
|
}
|
|
if (mode === "wsl") {
|
|
return wsl;
|
|
}
|
|
if (mode !== "auto") {
|
|
throw new Error(`Unsupported LIVESYNC_DOCKER_MODE='${mode}'. Use auto, native, or wsl.`);
|
|
}
|
|
|
|
// On Windows we prefer `wsl docker` first, then native docker.
|
|
// This typically works better in setups where Docker is installed only in
|
|
// WSL and not exposed as docker.exe on PATH.
|
|
const candidates = onWindows ? [wsl, native] : [native, wsl];
|
|
for (const c of candidates) {
|
|
if (c.bin === "docker") {
|
|
const r = await runCommand("docker", ["--version"]);
|
|
if (r.code === 0) return c;
|
|
continue;
|
|
}
|
|
const r = await runCommand("wsl", ["docker", "--version"]);
|
|
if (r.code === 0) return c;
|
|
}
|
|
|
|
throw new Error(
|
|
[
|
|
"Docker command is not available.",
|
|
"Set one of:",
|
|
"- LIVESYNC_DOCKER_MODE=native",
|
|
"- LIVESYNC_DOCKER_MODE=wsl",
|
|
"- LIVESYNC_DOCKER_COMMAND='docker'",
|
|
"- LIVESYNC_DOCKER_COMMAND='wsl docker'",
|
|
].join("\n")
|
|
);
|
|
}
|
|
|
|
async function getDockerInvoker(): Promise<DockerInvoker> {
|
|
if (!dockerInvokerPromise) {
|
|
dockerInvokerPromise = resolveDockerInvoker().then((r) => {
|
|
console.log(`[INFO] docker runner: ${r.label}`);
|
|
return r;
|
|
});
|
|
}
|
|
return await dockerInvokerPromise;
|
|
}
|
|
|
|
async function docker(...args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
const invoker = await getDockerInvoker();
|
|
|
|
// Either:
|
|
// docker <args>
|
|
// Or:
|
|
// wsl docker <args>
|
|
const finalArgs =
|
|
invoker.prefix.length === 0
|
|
? invoker.bin === "wsl"
|
|
? ["docker", ...args]
|
|
: args
|
|
: [...invoker.prefix, ...args];
|
|
|
|
const r = await runCommand(invoker.bin, finalArgs);
|
|
return { code: r.code, stdout: r.stdout, stderr: r.stderr };
|
|
}
|
|
|
|
async function dockerOrFail(...args: string[]): Promise<string> {
|
|
const r = await docker(...args);
|
|
if (r.code !== 0) {
|
|
throw new Error(`docker ${args[0]} failed (code ${r.code}): ${r.stderr.trim()}`);
|
|
}
|
|
return r.stdout;
|
|
}
|
|
|
|
async function stopAndRemoveContainer(container: string): Promise<void> {
|
|
await docker("stop", container).catch(() => {});
|
|
await docker("rm", container).catch(() => {});
|
|
}
|
|
|
|
async function cleanupTrackedContainers(reason: string): Promise<void> {
|
|
const names = [...trackedContainers];
|
|
if (names.length === 0) return;
|
|
|
|
console.warn(`[WARN] cleaning up tracked containers on ${reason}: ${names.join(", ")}`);
|
|
for (const container of names.reverse()) {
|
|
await stopAndRemoveContainer(container);
|
|
trackedContainers.delete(container);
|
|
}
|
|
}
|
|
|
|
async function handleSignalCleanup(signal: Deno.Signal): Promise<void> {
|
|
if (signalCleanupInProgress) return;
|
|
signalCleanupInProgress = true;
|
|
try {
|
|
await cleanupTrackedContainers(`signal ${signal}`);
|
|
} finally {
|
|
Deno.exit(signal === "SIGINT" ? 130 : 143);
|
|
}
|
|
}
|
|
|
|
function ensureSignalCleanupHandlers(): void {
|
|
if (signalCleanupHandlersInstalled) return;
|
|
signalCleanupHandlersInstalled = true;
|
|
for (const signal of CLEANUP_SIGNALS) {
|
|
const listener = () => {
|
|
void handleSignalCleanup(signal);
|
|
};
|
|
try {
|
|
Deno.addSignalListener(signal, listener);
|
|
signalCleanupHandlers.set(signal, listener);
|
|
} catch {
|
|
// Unsupported signal on this platform.
|
|
}
|
|
}
|
|
}
|
|
|
|
function removeSignalCleanupHandlers(): void {
|
|
if (!signalCleanupHandlersInstalled) return;
|
|
for (const [signal, listener] of signalCleanupHandlers) {
|
|
try {
|
|
Deno.removeSignalListener(signal, listener);
|
|
} catch {
|
|
// Ignore if already removed or unsupported.
|
|
}
|
|
}
|
|
signalCleanupHandlers.clear();
|
|
signalCleanupHandlersInstalled = false;
|
|
}
|
|
|
|
function trackContainer(container: string): void {
|
|
ensureSignalCleanupHandlers();
|
|
trackedContainers.add(container);
|
|
}
|
|
|
|
function untrackContainer(container: string): void {
|
|
trackedContainers.delete(container);
|
|
if (trackedContainers.size === 0) {
|
|
removeSignalCleanupHandlers();
|
|
}
|
|
}
|
|
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function waitForCouchdbStable(hostname: string, user: string, password: string): Promise<void> {
|
|
const h = hostname.replace(/\/$/, "").replace("localhost", "127.0.0.1");
|
|
const auth = btoa(`${user}:${password}`);
|
|
const headers = { Authorization: `Basic ${auth}` };
|
|
let consecutive = 0;
|
|
for (let i = 0; i < 30; i++) {
|
|
try {
|
|
const r = await fetch(`${h}/_up`, {
|
|
headers,
|
|
signal: AbortSignal.timeout(3000),
|
|
});
|
|
if (r.ok) {
|
|
consecutive++;
|
|
if (consecutive >= 3) return;
|
|
} else {
|
|
consecutive = 0;
|
|
}
|
|
} catch {
|
|
consecutive = 0;
|
|
}
|
|
await sleep(500);
|
|
}
|
|
throw new Error("CouchDB did not become stable in time");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Fetch with retry (mirrors cli_test_curl_json() retry loop)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function fetchRetry(
|
|
url: string,
|
|
init: RequestInit,
|
|
retries = 30,
|
|
delayMs = 2000,
|
|
allowStatus: number[] = []
|
|
): Promise<void> {
|
|
let lastError: unknown;
|
|
let lastStatus: number | undefined;
|
|
for (let i = 0; i < retries; i++) {
|
|
try {
|
|
const r = await fetch(url, {
|
|
signal: AbortSignal.timeout(5000),
|
|
...init,
|
|
});
|
|
lastStatus = r.status;
|
|
await r.body?.cancel().catch(() => {});
|
|
if (r.ok || allowStatus.includes(r.status)) return;
|
|
lastError = `HTTP ${r.status}`;
|
|
} catch (e) {
|
|
lastError = e;
|
|
}
|
|
await sleep(delayMs);
|
|
}
|
|
throw new Error(
|
|
`Could not reach ${url} after ${retries} retries: ${lastError} (last status: ${lastStatus ?? "N/A"})`
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CouchDB
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// TODO: these values could be configurable via environment variables.
|
|
//
|
|
const COUCHDB_CONTAINER = "couchdb-test";
|
|
const COUCHDB_IMAGE = "couchdb:3.5.0";
|
|
|
|
const MINIO_CONTAINER = "minio-test";
|
|
const MINIO_IMAGE = "minio/minio";
|
|
const MINIO_MC_IMAGE = "minio/mc";
|
|
|
|
export async function stopCouchdb(): Promise<void> {
|
|
await stopAndRemoveContainer(COUCHDB_CONTAINER);
|
|
untrackContainer(COUCHDB_CONTAINER);
|
|
}
|
|
|
|
/**
|
|
* Start a CouchDB test container, initialise it, and create the test DB.
|
|
* Mirrors cli_test_start_couchdb() from test-helpers.sh, using direct
|
|
* docker / fetch calls instead of the bash util scripts.
|
|
*/
|
|
export async function startCouchdb(couchdbUri: string, user: string, password: string, dbname: string): Promise<void> {
|
|
console.log("[INFO] stopping leftover CouchDB container if present");
|
|
await stopCouchdb().catch(() => {});
|
|
|
|
console.log("[INFO] starting CouchDB test container");
|
|
await dockerOrFail(
|
|
"run",
|
|
"-d",
|
|
"--name",
|
|
COUCHDB_CONTAINER,
|
|
"-p",
|
|
// TODO: port mapping should be configurable.
|
|
"5989:5984",
|
|
"-e",
|
|
`COUCHDB_USER=${user}`,
|
|
"-e",
|
|
`COUCHDB_PASSWORD=${password}`,
|
|
"-e",
|
|
"COUCHDB_SINGLE_NODE=y",
|
|
COUCHDB_IMAGE
|
|
);
|
|
trackContainer(COUCHDB_CONTAINER);
|
|
|
|
console.log("[INFO] initialising CouchDB");
|
|
await initCouchdb(couchdbUri, user, password);
|
|
|
|
console.log("[INFO] waiting for CouchDB to become stable");
|
|
await waitForCouchdbStable(couchdbUri, user, password);
|
|
|
|
console.log(`[INFO] creating test database: ${dbname}`);
|
|
await createCouchdbDatabase(couchdbUri, user, password, dbname);
|
|
}
|
|
|
|
/**
|
|
* Mirror couchdb-init.sh: configure single-node CouchDB via its REST API.
|
|
*/
|
|
async function initCouchdb(hostname: string, user: string, password: string, node = "_local"): Promise<void> {
|
|
// Podman environments often resolve localhost to ::1; use 127.0.0.1 like
|
|
// the bash script does.
|
|
const h = hostname.replace(/\/$/, "").replace("localhost", "127.0.0.1");
|
|
const auth = btoa(`${user}:${password}`);
|
|
const headers = {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Basic ${auth}`,
|
|
};
|
|
|
|
const calls: Array<[string, string, string]> = [
|
|
[
|
|
"POST",
|
|
`${h}/_cluster_setup`,
|
|
JSON.stringify({
|
|
action: "enable_single_node",
|
|
username: user,
|
|
password,
|
|
bind_address: "0.0.0.0",
|
|
port: 5984,
|
|
singlenode: true,
|
|
}),
|
|
],
|
|
["PUT", `${h}/_node/${node}/_config/chttpd/require_valid_user`, '"true"'],
|
|
["PUT", `${h}/_node/${node}/_config/chttpd_auth/require_valid_user`, '"true"'],
|
|
["PUT", `${h}/_node/${node}/_config/httpd/WWW-Authenticate`, '"Basic realm=\\"couchdb\\""'],
|
|
["PUT", `${h}/_node/${node}/_config/httpd/enable_cors`, '"true"'],
|
|
["PUT", `${h}/_node/${node}/_config/chttpd/enable_cors`, '"true"'],
|
|
["PUT", `${h}/_node/${node}/_config/chttpd/max_http_request_size`, '"4294967296"'],
|
|
["PUT", `${h}/_node/${node}/_config/couchdb/max_document_size`, '"50000000"'],
|
|
["PUT", `${h}/_node/${node}/_config/cors/credentials`, '"true"'],
|
|
["PUT", `${h}/_node/${node}/_config/cors/origins`, '"*"'],
|
|
];
|
|
|
|
for (const [method, url, body] of calls) {
|
|
await fetchRetry(url, { method, headers, body });
|
|
}
|
|
}
|
|
|
|
export async function createCouchdbDatabase(
|
|
hostname: string,
|
|
user: string,
|
|
password: string,
|
|
dbname: string
|
|
): Promise<void> {
|
|
const h = hostname.replace(/\/$/, "").replace("localhost", "127.0.0.1");
|
|
const auth = btoa(`${user}:${password}`);
|
|
await fetchRetry(`${h}/${dbname}`, {
|
|
method: "PUT",
|
|
headers: { Authorization: `Basic ${auth}` },
|
|
});
|
|
}
|
|
|
|
/** Update a CouchDB document via PUT. Returns the updated document. */
|
|
export async function updateCouchdbDoc(
|
|
hostname: string,
|
|
user: string,
|
|
password: string,
|
|
docUrl: string,
|
|
updater: (doc: Record<string, unknown>) => Record<string, unknown>
|
|
): Promise<void> {
|
|
const h = hostname.replace(/\/$/, "").replace("localhost", "127.0.0.1");
|
|
const auth = btoa(`${user}:${password}`);
|
|
const headers = {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Basic ${auth}`,
|
|
};
|
|
const getRes = await fetch(`${h}/${docUrl}`, { headers });
|
|
const current = (await getRes.json()) as Record<string, unknown>;
|
|
const updated = updater(current);
|
|
await fetchRetry(`${h}/${docUrl}`, {
|
|
method: "PUT",
|
|
headers,
|
|
body: JSON.stringify(updated),
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MinIO
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function shQuote(value: string): string {
|
|
return `'${value.split("'").join(`'"'"'`)}'`;
|
|
}
|
|
|
|
export async function stopMinio(): Promise<void> {
|
|
await stopAndRemoveContainer(MINIO_CONTAINER);
|
|
untrackContainer(MINIO_CONTAINER);
|
|
}
|
|
|
|
async function initMinioBucket(
|
|
minioEndpoint: string,
|
|
accessKey: string,
|
|
secretKey: string,
|
|
bucket: string
|
|
): Promise<boolean> {
|
|
const cmd =
|
|
`mc alias set myminio ${shQuote(minioEndpoint)} ${shQuote(accessKey)} ${shQuote(secretKey)} >/dev/null 2>&1 && ` +
|
|
`mc mb --ignore-existing myminio/${shQuote(bucket)} >/dev/null 2>&1`;
|
|
const r = await docker("run", "--rm", "--network", "host", "--entrypoint", "/bin/sh", MINIO_MC_IMAGE, "-c", cmd);
|
|
return r.code === 0;
|
|
}
|
|
|
|
async function waitForMinioBucket(
|
|
minioEndpoint: string,
|
|
accessKey: string,
|
|
secretKey: string,
|
|
bucket: string
|
|
): Promise<void> {
|
|
for (let i = 0; i < 30; i++) {
|
|
const checkCmd =
|
|
`mc alias set myminio ${shQuote(minioEndpoint)} ${shQuote(accessKey)} ${shQuote(secretKey)} >/dev/null 2>&1 && ` +
|
|
`mc ls myminio/${shQuote(bucket)} >/dev/null 2>&1`;
|
|
const check = await docker(
|
|
"run",
|
|
"--rm",
|
|
"--network",
|
|
// Now I used host networking to access the container via localhost for some environments (Docker Desktop on Windows).
|
|
// We need something good idea to work across all environments.
|
|
"host",
|
|
"--entrypoint",
|
|
"/bin/sh",
|
|
MINIO_MC_IMAGE,
|
|
"-c",
|
|
checkCmd
|
|
);
|
|
if (check.code === 0) {
|
|
return;
|
|
}
|
|
await initMinioBucket(minioEndpoint, accessKey, secretKey, bucket);
|
|
await sleep(2000);
|
|
}
|
|
throw new Error(`MinIO bucket not ready: ${bucket}`);
|
|
}
|
|
|
|
export async function startMinio(
|
|
minioEndpoint: string,
|
|
accessKey: string,
|
|
secretKey: string,
|
|
bucket: string
|
|
): Promise<void> {
|
|
console.log("[INFO] stopping leftover MinIO container if present");
|
|
await stopMinio().catch(() => {});
|
|
|
|
console.log("[INFO] starting MinIO test container");
|
|
await dockerOrFail(
|
|
"run",
|
|
"-d",
|
|
"--name",
|
|
MINIO_CONTAINER,
|
|
// TODO: Ports should be configurable.
|
|
"-p",
|
|
"9000:9000",
|
|
"-p",
|
|
"9001:9001",
|
|
"-e",
|
|
`MINIO_ROOT_USER=${accessKey}`,
|
|
"-e",
|
|
`MINIO_ROOT_PASSWORD=${secretKey}`,
|
|
"-e",
|
|
`MINIO_SERVER_URL=${minioEndpoint}`,
|
|
MINIO_IMAGE,
|
|
"server",
|
|
"/data",
|
|
"--console-address",
|
|
":9001"
|
|
);
|
|
trackContainer(MINIO_CONTAINER);
|
|
|
|
console.log(`[INFO] initialising MinIO test bucket: ${bucket}`);
|
|
let initialised = false;
|
|
for (let i = 0; i < 5; i++) {
|
|
if (await initMinioBucket(minioEndpoint, accessKey, secretKey, bucket)) {
|
|
initialised = true;
|
|
break;
|
|
}
|
|
await sleep(2000);
|
|
}
|
|
if (!initialised) {
|
|
throw new Error(`Could not initialise MinIO bucket after retries: ${bucket}`);
|
|
}
|
|
|
|
await waitForMinioBucket(minioEndpoint, accessKey, secretKey, bucket);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// P2P relay (strfry)
|
|
// ---------------------------------------------------------------------------
|
|
// TODO: these values could be configurable via environment variables.
|
|
const P2P_RELAY_CONTAINER = "relay-test";
|
|
const P2P_RELAY_IMAGE = "ghcr.io/hoytech/strfry:latest";
|
|
const STRFRY_BOOTSTRAP_SH = String.raw`cat > /tmp/strfry.conf <<"EOF"
|
|
db = "./strfry-db/"
|
|
|
|
relay {
|
|
bind = "0.0.0.0"
|
|
port = 7777
|
|
nofiles = 100000
|
|
|
|
info {
|
|
name = "livesync test relay"
|
|
description = "local relay for livesync p2p tests"
|
|
}
|
|
|
|
maxWebsocketPayloadSize = 131072
|
|
autoPingSeconds = 55
|
|
|
|
writePolicy {
|
|
plugin = ""
|
|
}
|
|
}
|
|
EOF
|
|
exec /app/strfry --config /tmp/strfry.conf relay`;
|
|
|
|
export async function stopP2pRelay(): Promise<void> {
|
|
await stopAndRemoveContainer(P2P_RELAY_CONTAINER);
|
|
untrackContainer(P2P_RELAY_CONTAINER);
|
|
}
|
|
|
|
/**
|
|
* Start the local P2P relay container through the same docker runner used
|
|
* by CouchDB helpers. This keeps process ownership consistent across
|
|
* start/stop on Windows, WSL, and native Linux/macOS.
|
|
*/
|
|
export async function startP2pRelay(): Promise<void> {
|
|
console.log("[INFO] stopping leftover P2P relay container if present");
|
|
await stopP2pRelay().catch(() => {});
|
|
|
|
console.log("[INFO] starting local P2P relay container");
|
|
await dockerOrFail(
|
|
"run",
|
|
"-d",
|
|
"--name",
|
|
P2P_RELAY_CONTAINER,
|
|
"-p",
|
|
//TODO: port mapping should be configurable.
|
|
"4000:7777",
|
|
"--tmpfs",
|
|
"/app/strfry-db:rw,size=256m",
|
|
"--entrypoint",
|
|
"sh",
|
|
P2P_RELAY_IMAGE,
|
|
"-lc",
|
|
STRFRY_BOOTSTRAP_SH
|
|
);
|
|
trackContainer(P2P_RELAY_CONTAINER);
|
|
}
|
|
|
|
export function isLocalP2pRelay(relayUrl: string): boolean {
|
|
return relayUrl.includes("localhost") || relayUrl.includes("127.0.0.1") || relayUrl.includes("[::1]");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Coturn (STUN/TURN)
|
|
// ---------------------------------------------------------------------------
|
|
const COTURN_CONTAINER = "coturn-test";
|
|
const COTURN_IMAGE = "coturn/coturn:latest";
|
|
|
|
export async function stopCoturn(): Promise<void> {
|
|
await stopAndRemoveContainer(COTURN_CONTAINER);
|
|
untrackContainer(COTURN_CONTAINER);
|
|
}
|
|
|
|
export async function startCoturn(
|
|
port = 3478,
|
|
user = "testuser",
|
|
pass = "testpass",
|
|
realm = "livesync.test"
|
|
): Promise<void> {
|
|
console.log("[INFO] stopping leftover Coturn container if present");
|
|
await stopCoturn().catch(() => {});
|
|
|
|
const { getOptimalLoopbackIp } = await import("./net.ts");
|
|
const externalIp = await getOptimalLoopbackIp();
|
|
|
|
console.log(`[INFO] starting local Coturn container with external-ip ${externalIp}`);
|
|
await dockerOrFail(
|
|
"run",
|
|
"-d",
|
|
"--name",
|
|
COTURN_CONTAINER,
|
|
"-p",
|
|
`${port}:${port}`,
|
|
"-p",
|
|
`${port}:${port}/udp`,
|
|
COTURN_IMAGE,
|
|
"--log-file=stdout",
|
|
`--external-ip=${externalIp}`,
|
|
`--user=${user}:${pass}`,
|
|
`--realm=${realm}`
|
|
);
|
|
trackContainer(COTURN_CONTAINER);
|
|
}
|