/ hole-punch-interop / src / compose-runner.ts
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  }