All files / kernel-cli/src utils.ts

100% Statements 34/34
100% Branches 21/21
100% Functions 10/10
100% Lines 33/33

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                    13x 1x   12x 8x   4x                     11x                               6x 6x 4x   2x 1x   1x                     3x 3x 3x   2x 1x   1x                           3x 3x 3x   2x 1x   1x                             3x 3x 5x 2x   3x   1x                           1x     1x   1x                    
import { readFile } from 'node:fs/promises';
 
/**
 * Parse and validate a `--timeout` option value (seconds → milliseconds).
 *
 * @param value - The raw option value from yargs.
 * @returns The timeout in milliseconds, or `undefined` if not provided.
 * @throws If the value is not a positive integer.
 */
export function parseTimeoutMs(value: number | undefined): number | undefined {
  if (value === undefined) {
    return undefined;
  }
  if (!Number.isInteger(value) || value <= 0) {
    throw new Error(`--timeout must be a positive integer, got: ${value}`);
  }
  return value * 1000;
}
 
/**
 * Check whether an unknown error is a Node.js system error with the given code.
 *
 * @param error - The error to check.
 * @param code - The expected error code (e.g. 'ENOENT', 'EPERM').
 * @returns True if the error matches the code.
 */
export function isErrorWithCode(error: unknown, code: string): boolean {
  return (
    error instanceof Error &&
    'code' in error &&
    (error as NodeJS.ErrnoException).code === code
  );
}
 
/**
 * Read a PID from a file.
 *
 * @param pidPath - The PID file path.
 * @returns The PID, or undefined if the file is missing or invalid.
 */
export async function readPidFile(
  pidPath: string,
): Promise<number | undefined> {
  try {
    const pid = Number(await readFile(pidPath, 'utf-8'));
    return pid > 0 && !Number.isNaN(pid) ? pid : undefined;
  } catch (error: unknown) {
    if (isErrorWithCode(error, 'ENOENT')) {
      return undefined;
    }
    throw error;
  }
}
 
/**
 * Check whether a process is alive by sending signal 0.
 *
 * @param pid - The process ID to check.
 * @returns True if the process exists.
 */
export function isProcessAlive(pid: number): boolean {
  try {
    process.kill(pid, 0);
    return true;
  } catch (error: unknown) {
    if (isErrorWithCode(error, 'EPERM')) {
      return true;
    }
    return false;
  }
}
 
/**
 * Send a signal to a process. Returns true if the signal was sent, false if
 * the process does not exist (ESRCH). Re-throws on permission errors (EPERM)
 * and other failures.
 *
 * @param pid - The process ID.
 * @param signal - The signal to send.
 * @returns True if the signal was delivered, false if the process is gone.
 */
export function sendSignal(pid: number, signal: NodeJS.Signals): boolean {
  try {
    process.kill(pid, signal);
    return true;
  } catch (error: unknown) {
    if (isErrorWithCode(error, 'ESRCH')) {
      return false;
    }
    throw error;
  }
}
 
/**
 * Poll until a condition is met or the timeout elapses.
 *
 * @param check - A function that returns true when the condition is met.
 * @param timeoutMs - Maximum time to wait in milliseconds.
 * @returns True if the condition was met, false on timeout.
 */
export async function waitFor(
  check: () => boolean | Promise<boolean>,
  timeoutMs: number,
): Promise<boolean> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (await check()) {
      return true;
    }
    await new Promise((resolve) => setTimeout(resolve, 250));
  }
  return await check();
}
 
/**
 * Wrap a promise with a timeout rejection.
 *
 * @param promise - The promise to wrap with a timeout.
 * @param timeout - How many ms to wait before rejecting.
 * @returns A wrapped promise which rejects after timeout miliseconds.
 */
export async function withTimeout<Return>(
  promise: Promise<Return>,
  timeout: number,
): Promise<Return> {
  return Promise.race([
    promise,
    new Promise((_resolve, reject) =>
      setTimeout(
        () =>
          reject(
            new Error(`promise timed out after ${timeout}ms`, {
              cause: promise,
            }),
          ),
        timeout,
      ),
    ),
  ]) as Promise<Return>;
}