diff --git a/lib/upload-audit.js b/lib/upload-audit.js index eade08c..7234d42 100644 --- a/lib/upload-audit.js +++ b/lib/upload-audit.js @@ -42,7 +42,7 @@ function createInternalLogWriter(options) { const maxBackups = Number.isFinite(source.maxBackups) ? source.maxBackups : 2; let activePath = null; - if (!fs || !fs.promises || typeof fs.promises.appendFile !== 'function' || typeof resolveInternalLogPath !== 'function' || typeof fileName !== 'string') { + if (!fs || !fs.promises || typeof fs.promises.appendFile !== 'function' || typeof fs.appendFileSync !== 'function' || typeof resolveInternalLogPath !== 'function' || typeof fileName !== 'string') { throw new TypeError('createInternalLogWriter requires fs, fileName and resolveInternalLogPath'); } @@ -66,8 +66,36 @@ function createInternalLogWriter(options) { return false; } + function appendSync(line, label) { + const excludedPaths = new Set(); + for (let attempt = 0; attempt < retryDelays.length; attempt++) { + const targetPath = resolveInternalLogPath(fileName, excludedPaths); + if (!targetPath) break; + excludedPaths.add(targetPath); + try { + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + rotateLogFile(targetPath, maxBytes, maxBackups); + fs.appendFileSync(targetPath, line, 'utf-8'); + activePath = targetPath; + return true; + } catch (error) { + reportError(label, error); + } + } + return false; + } + + function flushSync(buffer, label) { + if (!Array.isArray(buffer) || buffer.length === 0) return true; + if (!appendSync(buffer.join(''), label)) return false; + buffer.length = 0; + return true; + } + return { append, + appendSync, + flushSync, getActivePath: () => activePath, getPath: () => activePath || resolveInternalLogPath(fileName) }; diff --git a/main.js b/main.js index 3ddff45..6911d2e 100644 --- a/main.js +++ b/main.js @@ -1785,8 +1785,7 @@ app.on('will-quit', () => { } catch {} try { if (_rotLogBuffer.length) { - fs.appendFileSync(getRotLogPath(), _rotLogBuffer.join(''), 'utf-8'); - _rotLogBuffer.length = 0; + _rotLogWriter.flushSync(_rotLogBuffer, 'rot-log'); } } catch {} }); diff --git a/tests/upload-audit.test.js b/tests/upload-audit.test.js index 41add76..84266c9 100644 --- a/tests/upload-audit.test.js +++ b/tests/upload-audit.test.js @@ -55,6 +55,7 @@ test('internal rotation writer retries inside userData and reports the file that const rotationTargets = []; const testFs = { mkdirSync: fs.mkdirSync, + appendFileSync: fs.appendFileSync, promises: { appendFile: async (targetPath, ...args) => { appendTargets.push(targetPath); @@ -82,6 +83,55 @@ test('internal rotation writer retries inside userData and reports the file that assert.equal(fs.readFileSync(fallbackPath, 'utf8'), '[rotation]\n'); }); +test('synchronous rotation flush falls back completely and retains buffered lines when every target fails', (t) => { + const directory = createTempDirectory(t, 'mhu-internal-log-sync-writer-'); + const userDataPath = path.join(directory, 'user-data'); + const primaryPath = path.join(userDataPath, 'logs', 'account-rotation.log'); + const fallbackPath = path.join(userDataPath, 'internal-logs', 'account-rotation.log'); + fs.mkdirSync(primaryPath, { recursive: true }); + const resolveInternalLogPath = createInternalLogPathResolver({ fs, path, userDataPath }); + const writer = createInternalLogWriter({ + fs, + path, + fileName: 'account-rotation.log', + resolveInternalLogPath, + rotateLogFile: () => {}, + reportError: () => {} + }); + const buffer = ['[rotation 1]\n', '[rotation 2]\n']; + + assert.equal(writer.flushSync(buffer, 'rot-log'), true); + assert.deepEqual(buffer, []); + assert.equal(writer.getActivePath(), fallbackPath); + assert.equal(fs.readFileSync(fallbackPath, 'utf8'), '[rotation 1]\n[rotation 2]\n'); + + const failedUserDataPath = path.join(directory, 'failed-user-data'); + const failedPrimaryPath = path.join(failedUserDataPath, 'logs', 'account-rotation.log'); + const failedFallbackPath = path.join(failedUserDataPath, 'internal-logs', 'account-rotation.log'); + fs.mkdirSync(failedPrimaryPath, { recursive: true }); + fs.mkdirSync(failedFallbackPath, { recursive: true }); + const failedWriter = createInternalLogWriter({ + fs, + path, + fileName: 'account-rotation.log', + resolveInternalLogPath: createInternalLogPathResolver({ fs, path, userDataPath: failedUserDataPath }), + rotateLogFile: () => {}, + reportError: () => {} + }); + const retainedBuffer = ['[retained 1]\n', '[retained 2]\n']; + + assert.equal(failedWriter.flushSync(retainedBuffer, 'rot-log'), false); + assert.deepEqual(retainedBuffer, ['[retained 1]\n', '[retained 2]\n']); + assert.equal(failedWriter.getActivePath(), null); +}); + +test('quit flush delegates the rotation buffer to the synchronous internal writer', () => { + const mainSource = fs.readFileSync(path.join(__dirname, '..', 'main.js'), 'utf8'); + + assert.match(mainSource, /_rotLogWriter\.flushSync\(_rotLogBuffer, 'rot-log'\);/); + assert.doesNotMatch(mainSource, /appendFileSync\(getRotLogPath\(\), _rotLogBuffer\.join\(''\)/); +}); + test('upload audit writer leaves the configured fileuploader log contract unchanged', async (t) => { const directory = createTempDirectory(t, 'mhu-upload-log-contract-'); const userDataPath = path.join(directory, 'user-data');