Files
learnharness/electron/desktop-logger.cjs
T

117 lines
3.8 KiB
JavaScript

/** Bounded desktop diagnostics. Error paths must never write to a failed pipe. */
const fs = require("node:fs");
const path = require("node:path");
const MAX_LOG_BYTES = 5 * 1024 * 1024;
function createDesktopLogger({ directory, stdout, stderr, filesystem = fs, maxFileBytes = MAX_LOG_BYTES }) {
let initialized = false;
let stdoutEnabled = Boolean(stdout);
let writing = false;
const location = () => path.join(directory(), "main.log");
// Read only the retained tail, even when an older release left a huge file.
function trim(file) {
let handle;
try {
handle = filesystem.openSync(file, "r+");
const size = filesystem.fstatSync(handle).size;
if (size <= maxFileBytes) return;
const tail = Buffer.alloc(maxFileBytes);
let read = 0;
while (read < tail.length) {
const count = filesystem.readSync(handle, tail, read, tail.length - read, size - tail.length + read);
if (!count) break;
read += count;
}
filesystem.writeFileSync(handle, tail.subarray(0, read));
filesystem.ftruncateSync(handle, read);
} catch (error) {
if (error.code !== "ENOENT") throw error;
} finally {
if (handle !== undefined) filesystem.closeSync(handle);
}
}
function initialize() {
try {
filesystem.mkdirSync(directory(), { recursive: true });
trim(location());
trim(`${location()}.1`);
initialized = true;
} catch {
// Diagnostics are best effort; never report a logger failure via itself.
}
}
function record(line) {
const prefix = Buffer.from(`[${new Date().toISOString()}] `);
const body = Buffer.from(String(line));
if (prefix.length + body.length + 1 <= maxFileBytes) return Buffer.concat([prefix, body, Buffer.from("\n")]);
const marker = Buffer.from("[truncated] ");
const tail = body.subarray(body.length - (maxFileBytes - prefix.length - marker.length - 1));
// A retained suffix can start inside a UTF-8 character.
let start = 0;
while (start < tail.length && (tail[start] & 0xc0) === 0x80) start++;
return Buffer.concat([prefix, marker, tail.subarray(start), Buffer.from("\n")]);
}
function append(entry) {
try {
if (!initialized) initialize();
if (!initialized) return;
const file = location();
let size = 0;
try { size = filesystem.statSync(file).size; } catch (error) { if (error.code !== "ENOENT") throw error; }
if (size + entry.length > maxFileBytes) {
filesystem.rmSync(`${file}.1`, { force: true });
filesystem.renameSync(file, `${file}.1`);
}
filesystem.appendFileSync(file, entry);
} catch {
// Includes ENOSPC and rotation failures. Do not fall back to a pipe.
}
}
function log(line, { fileOnly = false } = {}) {
if (writing) return;
writing = true;
try {
const entry = record(line);
append(entry);
if (!fileOnly && stdoutEnabled && !stdout.destroyed && !stdout.writableEnded) {
try {
// A false return means backpressure: skip mirroring until drained.
if (!stdout.write(entry, (error) => { if (error) disableStdout(error); })) stdoutEnabled = false;
} catch (error) {
disableStdout(error);
}
}
} catch {
// Formatting must not escape into the uncaught-exception handler either.
} finally {
writing = false;
}
}
let stdoutFailed = false;
function disableStdout(error) {
stdoutEnabled = false;
if (stdoutFailed) return;
stdoutFailed = true;
append(record(`stdout disabled: ${error?.code ?? error?.message ?? "stream error"}`));
}
stdout?.on("error", disableStdout);
stdout?.on("drain", () => { if (!stdoutFailed) stdoutEnabled = true; });
// Console / dependency writes to stderr can fail asynchronously as well.
let stderrFailed = false;
stderr?.on("error", (error) => {
if (stderrFailed) return;
stderrFailed = true;
log(`stderr disabled: ${error?.code ?? error?.message ?? "stream error"}`, { fileOnly: true });
});
return { log, initialize };
}
module.exports = { createDesktopLogger, MAX_LOG_BYTES };