compose-runner.ts
1 import {promises as fs} from 'fs'; 2 import path from 'path'; 3 import {exec as execStd} from 'child_process'; 4 import util from 'util'; 5 import {ComposeSpecification} from "../compose-spec/compose-spec"; 6 import {stringify} from 'yaml'; 7 import {sanitizeComposeName} from "./lib"; 8 9 const exec = util.promisify(execStd); 10 11 export async function run(compose: ComposeSpecification, rootAssetDir: string, dryRun: boolean): Promise<Report | null> { 12 const sanitizedComposeName = sanitizeComposeName(compose.name) 13 const assetDir = path.join(rootAssetDir, sanitizedComposeName); 14 15 await fs.mkdir(assetDir, { recursive: true }) 16 17 18 // Create compose.yaml file 19 // Some docker compose environments don't like the name field to have special characters 20 const composeYmlPath = path.join(assetDir, "docker-compose.yaml"); 21 await fs.writeFile(composeYmlPath, stringify({ ...compose, name: sanitizedComposeName })) 22 23 if (dryRun) { 24 return null; 25 } 26 27 const stdoutLogFile = path.join(assetDir, `stdout.log`); 28 const stderrLogFile = path.join(assetDir, `stderr.log`); 29 30 try { 31 const { stdout, stderr } = await exec(`docker compose -f ${composeYmlPath} up --exit-code-from dialer --abort-on-container-exit`, { timeout: 60 * 1000 }) 32 33 await fs.writeFile(stdoutLogFile, stdout); 34 await fs.writeFile(stderrLogFile, stderr); 35 36 return JSON.parse(lastStdoutLine(stdout, "dialer", sanitizedComposeName)) as Report 37 } catch (e: unknown) { 38 if (isExecException(e)) { 39 await fs.writeFile(stdoutLogFile, e.stdout) 40 await fs.writeFile(stderrLogFile, e.stderr) 41 } 42 43 throw e 44 } finally { 45 try { 46 await exec(`docker compose -f ${composeYmlPath} down`); 47 } catch (e) { 48 console.log("Failed to compose down", e) 49 } 50 } 51 } 52 53 export interface ExecException extends Error { 54 cmd?: string | undefined; 55 killed?: boolean | undefined; 56 code?: number | undefined; 57 signal?: NodeJS.Signals | undefined; 58 stdout: string; 59 stderr: string; 60 } 61 62 function isExecException(candidate: unknown): candidate is ExecException { 63 return candidate && typeof candidate === 'object' && 'cmd' in candidate; 64 } 65 66 interface Report { 67 rtt_to_holepunched_peer_millis: number 68 } 69 70 export function lastStdoutLine(stdout: string, component: string, composeName: string): string { 71 const allComponentStdout = stdout.split("\n").filter(line => line.startsWith(`${composeName}-${component}-1`)); 72 73 const exitMessage = allComponentStdout.pop(); 74 const lastLine = allComponentStdout.pop(); 75 76 const [front, componentStdout] = lastLine.split("|"); 77 78 return componentStdout.trim() 79 }