All files / kernel-cli/src/commands relay.ts

86.44% Statements 51/59
96.66% Branches 29/30
46.66% Functions 7/15
91.07% Lines 51/56

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155                            26x                 9x             7x                         5x   5x 5x 1x     4x     4x 4x   1x 1x     3x 3x   3x   5x 1x       2x   1x 1x 1x     2x                     2x 2x             3x 3x 1x   2x 1x   2x 2x                             7x   7x 2x 1x   2x 2x     5x 5x     5x 5x 3x       4x 1x 1x 1x       4x 3x 3x   1x   4x    
import { startRelay } from '@metamask/kernel-utils/libp2p';
import type { Logger } from '@metamask/logger';
import { mkdir, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
 
import { getOcapHome } from '../ocap-home.ts';
import { isProcessAlive, readPidFile, sendSignal, waitFor } from '../utils.ts';
 
/**
 * Get the relay PID file path.
 *
 * @returns The relay PID file path.
 */
export function getRelayPidPath(): string {
  return join(getOcapHome(), 'relay.pid');
}
 
/**
 * Get the relay address file path.
 *
 * @returns The relay address file path.
 */
export function getRelayAddrPath(): string {
  return join(getOcapHome(), 'relay.addr');
}
 
/**
 * Remove the relay PID and address files.
 */
async function removeRelayFiles(): Promise<void> {
  await Promise.all([
    rm(getRelayPidPath(), { force: true }),
    rm(getRelayAddrPath(), { force: true }),
  ]);
}
 
/**
 * Start the relay server, write a PID file, and register signal handlers for
 * cleanup on exit.
 *
 * @param logger - The logger instance.
 */
export async function startRelayWithBookkeeping(logger: Logger): Promise<void> {
  await mkdir(getOcapHome(), { recursive: true });
 
  const existingPid = await readPidFile(getRelayPidPath());
  if (existingPid !== undefined && isProcessAlive(existingPid)) {
    throw new Error(`Relay is already running (PID: ${existingPid}).`);
  }
 
  await writeFile(getRelayPidPath(), String(process.pid));
 
  let libp2p;
  try {
    libp2p = await startRelay(logger);
  } catch (error) {
    await removeRelayFiles();
    throw error;
  }
 
  try {
    const relayAddr = libp2p
      .getMultiaddrs()
      .find((ma) => ma.toString().includes('/ip4/127.0.0.1/tcp/9001/ws/'))
      ?.toString();
    if (relayAddr === undefined) {
      throw new Error(
        'Relay started but no WS multiaddr found on 127.0.0.1:9001',
      );
    }
    await writeFile(getRelayAddrPath(), relayAddr);
  } catch (error) {
    await Promise.resolve(libp2p.stop()).catch(() => undefined);
    await removeRelayFiles();
    throw error;
  }
 
  const cleanup = (): void => {
    Promise.resolve(libp2p.stop())
      .catch(() => undefined)
      .finally(() => {
        removeRelayFiles()
          .catch(() => undefined)
          // eslint-disable-next-line n/no-process-exit -- signal handler must force exit after cleanup
          .finally(() => process.exit(0));
      });
  };
 
  process.on('SIGTERM', cleanup);
  process.on('SIGINT', cleanup);
}
 
/**
 * Print whether the relay process is running.
 */
export async function printRelayStatus(): Promise<void> {
  const pid = await readPidFile(getRelayPidPath());
  if (pid !== undefined && isProcessAlive(pid)) {
    process.stderr.write(`Relay is running (PID: ${pid}).\n`);
  } else {
    if (pid !== undefined) {
      await removeRelayFiles();
    }
    process.stderr.write('Relay is not running.\n');
    process.exitCode = 1;
  }
}
 
/**
 * Stop the relay process. Sends SIGTERM and waits; escalates to SIGKILL if
 * `force` is true and SIGTERM is ignored.
 *
 * @param options - Options.
 * @param options.force - Send SIGKILL if SIGTERM fails to stop the relay.
 * @returns True if the relay was stopped (or was not running), false otherwise.
 */
export async function stopRelay({
  force = false,
}: { force?: boolean } = {}): Promise<boolean> {
  const pid = await readPidFile(getRelayPidPath());
 
  if (pid === undefined || !isProcessAlive(pid)) {
    if (pid !== undefined) {
      await removeRelayFiles();
    }
    process.stderr.write('Relay is not running.\n');
    return true;
  }
 
  process.stderr.write('Stopping relay...\n');
  let stopped = false;
 
  // Strategy 1: SIGTERM.
  stopped = !sendSignal(pid, 'SIGTERM');
  if (!stopped) {
    stopped = await waitFor(() => !isProcessAlive(pid), 5_000);
  }
 
  // Strategy 2: SIGKILL (only with --force).
  if (!stopped && force) {
    stopped = !sendSignal(pid, 'SIGKILL');
    Eif (!stopped) {
      stopped = await waitFor(() => !isProcessAlive(pid), 2_000);
    }
  }
 
  if (stopped) {
    await removeRelayFiles();
    process.stderr.write('Relay stopped.\n');
  } else {
    process.stderr.write('Relay did not stop within timeout.\n');
  }
  return stopped;
}