User report: in session mode the upload-log lines split across two files — the first few before the auto-persist fallback fired, the rest after into a path with the session-stamp DOUBLED. Root cause in main.js _persistFallbackLogPath: 1. The strip was gated on the legacy `sessionLog` boolean, which 3.3.35 retired in favour of `logMode`. So in session/daily mode the gate was false and the resolved path got persisted with its stamp intact. 2. Even when the gate triggered, its regex matched only the daily YYYY-MM-DD suffix, not the session "session-YYYY-MM-DD_HH-MM-SS-pid" suffix. The next getLogFilePath() call read that saved path as the "base", treated the already-stamped filename as the base name, and re-applied another stamp on top. First flush hit the original session file; everything after hit a doubly- stamped one — exactly the symptom (top file: 2 lines, bottom file: the rest). - lib/log-mode.js: new pure stripModeStampFromFileName helper that removes both the daily and the session suffix patterns. Anchored to $, no nested quantifiers (linear). - main.js: gate on logMode (not sessionLog) and call the helper for daily AND session, so logFilePath always persists as a bare base. - Tests: 4 new — strip behaviour + an idempotence regression that locks in "resolve → strip → resolve = same path" so this can't silently come back. 204/200. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
108 lines
4.8 KiB
JavaScript
108 lines
4.8 KiB
JavaScript
// Log-file mode resolution for fileuploader.log:
|
|
// - "single" → one file: fileuploader.log
|
|
// - "daily" → per-day: fileuploader-YYYY-MM-DD.log
|
|
// - "session" → per-launch: fileuploader-session-YYYY-MM-DD_HH-MM-SS-<pid>.log
|
|
//
|
|
// Pure functions only — no fs, no Date.now() at call time — so they unit-test
|
|
// cleanly and the main.js call sites pass in `new Date()` + the session stamp.
|
|
//
|
|
// MIGRATION TRAP this lib protects against: the legacy boolean was named
|
|
// `sessionLog` but actually toggled *daily* mode. A naive rename would silently
|
|
// flip every per-day user onto per-session. normalizeLogMode below maps the
|
|
// legacy `sessionLog: true` to "daily", NOT "session". Read logMode everywhere
|
|
// downstream; do not derive from sessionLog at call sites.
|
|
//
|
|
// Loaded both as CommonJS (main.js, tests) and as a browser global
|
|
// (renderer/app.js via index.html script tag) so a single implementation backs
|
|
// runtime and tests — same pattern as queue-prune.js / queue-dedup.js.
|
|
|
|
(function (root) {
|
|
'use strict';
|
|
|
|
const VALID_MODES = new Set(['single', 'daily', 'session']);
|
|
|
|
function normalizeLogMode(globalSettings) {
|
|
const gs = globalSettings && typeof globalSettings === 'object' ? globalSettings : {};
|
|
if (typeof gs.logMode === 'string' && VALID_MODES.has(gs.logMode)) {
|
|
return gs.logMode;
|
|
}
|
|
// Legacy boolean migration: sessionLog *named* like "session" but actually
|
|
// implemented "daily" — preserve daily users on the migration path.
|
|
if (gs.sessionLog === true) return 'daily';
|
|
return 'single';
|
|
}
|
|
|
|
function _two(n) { return String(n).padStart(2, '0'); }
|
|
|
|
function formatDateStamp(date) {
|
|
return `${date.getFullYear()}-${_two(date.getMonth() + 1)}-${_two(date.getDate())}`;
|
|
}
|
|
|
|
function formatSessionStamp(date, pid) {
|
|
const d = `${date.getFullYear()}-${_two(date.getMonth() + 1)}-${_two(date.getDate())}`;
|
|
const t = `${_two(date.getHours())}-${_two(date.getMinutes())}-${_two(date.getSeconds())}`;
|
|
// PID disambiguates a same-second close→reopen — a human can't but two
|
|
// automated runs might. Cheap belt to a suspenders-not-required problem.
|
|
const pidStr = pid !== undefined && pid !== null ? `-${pid}` : '';
|
|
return `${d}_${t}${pidStr}`;
|
|
}
|
|
|
|
/**
|
|
* Compute the log filename for the given mode + clock.
|
|
* @param {Object} args
|
|
* @param {string} args.baseName e.g. "fileuploader"
|
|
* @param {string} args.ext e.g. ".log"
|
|
* @param {string} args.mode "single" | "daily" | "session"
|
|
* @param {Date} args.date current timestamp
|
|
* @param {string} [args.sessionId] required when mode === "session"
|
|
* @returns {string} the bare filename (no directory)
|
|
*/
|
|
function resolveLogFileName(args) {
|
|
const a = args || {};
|
|
const base = String(a.baseName || 'fileuploader');
|
|
const ext = String(a.ext || '.log');
|
|
const mode = VALID_MODES.has(a.mode) ? a.mode : 'single';
|
|
if (mode === 'single') return `${base}${ext}`;
|
|
if (mode === 'daily') {
|
|
const date = a.date instanceof Date ? a.date : new Date();
|
|
return `${base}-${formatDateStamp(date)}${ext}`;
|
|
}
|
|
// session
|
|
const sid = a.sessionId && String(a.sessionId).trim();
|
|
if (sid) return `${base}-session-${sid}${ext}`;
|
|
// Defensive: if a session-id wasn't passed, fall back to single rather
|
|
// than emit a malformed name. main.js always supplies one.
|
|
return `${base}${ext}`;
|
|
}
|
|
|
|
/**
|
|
* Reverse of resolveLogFileName: given a full filename like
|
|
* "fileuploader-2026-06-03.log" or
|
|
* "fileuploader-session-2026-06-03_18-16-20-8132.log", strip the mode-stamp
|
|
* so the bare base ("fileuploader.log") remains. Used when persisting an
|
|
* auto-resolved fallback path back into config — otherwise the saved path
|
|
* would keep growing a new stamp on every reload.
|
|
*/
|
|
function stripModeStampFromFileName(fileName) {
|
|
if (!fileName || typeof fileName !== 'string') return fileName;
|
|
// Order matters: session first (longer, more specific) before daily.
|
|
// Both regexes are anchored to $ with no nested/ambiguous quantifiers, so
|
|
// matching is linear — the eslint security warning is precautionary.
|
|
// eslint-disable-next-line security/detect-unsafe-regex
|
|
const sessionRe = /-session-\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(?:-\d+)?(\.[^.]+)?$/;
|
|
// eslint-disable-next-line security/detect-unsafe-regex
|
|
const dailyRe = /-\d{4}-\d{2}-\d{2}(\.[^.]+)?$/;
|
|
let out = fileName.replace(sessionRe, (m, ext) => ext || '');
|
|
out = out.replace(dailyRe, (m, ext) => ext || '');
|
|
return out;
|
|
}
|
|
|
|
const api = { normalizeLogMode, resolveLogFileName, formatDateStamp, formatSessionStamp, stripModeStampFromFileName, VALID_MODES };
|
|
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = api;
|
|
} else if (root) {
|
|
root.LogMode = api;
|
|
}
|
|
})(typeof window !== 'undefined' ? window : this);
|