File size: 4,361 Bytes
d197cf3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/** Reads recent gateway service logs for actionable daemon restart diagnostics. */
import fs, { type FileHandle } from "node:fs/promises";
import { readFileWindowFully } from "../infra/file-read.js";
import { resolveGatewayLogPaths, resolveGatewaySupervisorLogPaths } from "./restart-logs.js";

// Error patterns worth surfacing from gateway service logs after failed starts.
const GATEWAY_LOG_ERROR_PATTERNS = [
  /\bENOSPC\b/i,
  /no space left on device/i,
  /refusing to bind gateway/i,
  /gateway auth mode/i,
  /gateway start blocked/i,
  /failed to bind gateway socket/i,
  /tailscale .* requires/i,
];

const GATEWAY_DIAGNOSTIC_LOG_TAIL_BYTES = 256 * 1024;

async function readTailWindow(handle: FileHandle, size: number) {
  const length = Math.min(size, GATEWAY_DIAGNOSTIC_LOG_TAIL_BYTES);
  const readStart = size - length;
  const buffer = Buffer.alloc(length);
  const bytesRead = await readFileWindowFully(handle, buffer, readStart);
  return { buffer, bytesRead, readStart };
}

/** Reads complete lines from a bounded gateway log tail. */
export async function readGatewayLogTailLines(filePath: string): Promise<string[]> {
  const handle = await fs.open(filePath, "r");
  try {
    const stat = await handle.stat();
    if (!stat.isFile() || stat.size <= 0) {
      return [];
    }
    let window = await readTailWindow(handle, stat.size);
    if (window.bytesRead < window.buffer.length) {
      const refreshedStat = await handle.stat();
      if (!refreshedStat.isFile() || refreshedStat.size <= 0) {
        return [];
      }
      if (refreshedStat.size !== stat.size) {
        window = await readTailWindow(handle, refreshedStat.size);
      }
    }
    const { buffer, bytesRead, readStart } = window;
    let textStart = 0;
    if (readStart > 0) {
      const precedingByte = Buffer.alloc(1);
      const precedingRead = await handle.read(precedingByte, 0, 1, readStart - 1);
      if (precedingRead.bytesRead !== 1 || precedingByte[0] !== 0x0a) {
        // A byte-bound tail can start inside a line or UTF-8 sequence. Drop that
        // fragment so diagnostics never report a stale, corrupted partial line.
        const firstNewline = buffer.subarray(0, bytesRead).indexOf(0x0a);
        if (firstNewline === -1) {
          return [];
        }
        textStart = firstNewline + 1;
      }
    }
    const lines = buffer.subarray(textStart, bytesRead).toString("utf8").split(/\r?\n/u);
    if (lines.at(-1) === "") {
      lines.pop();
    }
    return lines;
  } finally {
    await handle.close();
  }
}

function findLastNonEmptyLine(lines: string[]): string | null {
  for (let i = lines.length - 1; i >= 0; i -= 1) {
    const line = lines[i]?.trim();
    if (line) {
      return line;
    }
  }
  return null;
}

export async function readLastGatewayErrorLine(
  env: NodeJS.ProcessEnv,
  options?: { platform?: NodeJS.Platform; requirePatternMatch?: boolean },
): Promise<string | null> {
  const platform = options?.platform ?? process.platform;
  const readStderr = platform !== "darwin";
  // launchd combines child stderr into stdout only because the plist points both
  // handles at one file (buildLaunchAgentPlist); break that and darwin startup
  // crashes stop reaching this reader. Other platforms keep stderr separate.
  const { stdoutPath, stderrPath } =
    platform === "darwin"
      ? resolveGatewaySupervisorLogPaths(env, { platform })
      : resolveGatewayLogPaths(env);
  const stderrLines = readStderr ? await readGatewayLogTailLines(stderrPath).catch(() => []) : [];
  const stdoutLines = await readGatewayLogTailLines(stdoutPath).catch(() => []);
  // stderr is the strongest failure signal on non-darwin platforms, so place it
  // last and scan from the end: the most recent stderr error line then wins over
  // any (possibly stale) stdout match, matching the stderr-first fallback below.
  const lines = [...stdoutLines, ...stderrLines].map((line) => line.trim());
  for (let i = lines.length - 1; i >= 0; i -= 1) {
    const line = lines[i];
    if (!line) {
      continue;
    }
    if (GATEWAY_LOG_ERROR_PATTERNS.some((pattern) => pattern.test(line))) {
      return line;
    }
  }
  if (options?.requirePatternMatch) {
    return null;
  }
  return readStderr
    ? (findLastNonEmptyLine(stderrLines) ?? findLastNonEmptyLine(stdoutLines))
    : findLastNonEmptyLine(stdoutLines);
}