fix: preserve rotation logs during quit fallback
Route synchronous account-rotation shutdown flushes through the internal log writer so failures under userData/logs retry userData/internal-logs in the same order as runtime writes. Keep buffered lines until a target accepts the full chunk, report the successful fallback as the active path, and retain the buffer when every internal target fails. Cover the real EISDIR primary-path case, complete fallback failure, and main-process quit delegation without launching Electron.
This commit is contained in:
+29
-1
@@ -42,7 +42,7 @@ function createInternalLogWriter(options) {
|
|||||||
const maxBackups = Number.isFinite(source.maxBackups) ? source.maxBackups : 2;
|
const maxBackups = Number.isFinite(source.maxBackups) ? source.maxBackups : 2;
|
||||||
let activePath = null;
|
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');
|
throw new TypeError('createInternalLogWriter requires fs, fileName and resolveInternalLogPath');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,8 +66,36 @@ function createInternalLogWriter(options) {
|
|||||||
return false;
|
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 {
|
return {
|
||||||
append,
|
append,
|
||||||
|
appendSync,
|
||||||
|
flushSync,
|
||||||
getActivePath: () => activePath,
|
getActivePath: () => activePath,
|
||||||
getPath: () => activePath || resolveInternalLogPath(fileName)
|
getPath: () => activePath || resolveInternalLogPath(fileName)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1785,8 +1785,7 @@ app.on('will-quit', () => {
|
|||||||
} catch {}
|
} catch {}
|
||||||
try {
|
try {
|
||||||
if (_rotLogBuffer.length) {
|
if (_rotLogBuffer.length) {
|
||||||
fs.appendFileSync(getRotLogPath(), _rotLogBuffer.join(''), 'utf-8');
|
_rotLogWriter.flushSync(_rotLogBuffer, 'rot-log');
|
||||||
_rotLogBuffer.length = 0;
|
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ test('internal rotation writer retries inside userData and reports the file that
|
|||||||
const rotationTargets = [];
|
const rotationTargets = [];
|
||||||
const testFs = {
|
const testFs = {
|
||||||
mkdirSync: fs.mkdirSync,
|
mkdirSync: fs.mkdirSync,
|
||||||
|
appendFileSync: fs.appendFileSync,
|
||||||
promises: {
|
promises: {
|
||||||
appendFile: async (targetPath, ...args) => {
|
appendFile: async (targetPath, ...args) => {
|
||||||
appendTargets.push(targetPath);
|
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');
|
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) => {
|
test('upload audit writer leaves the configured fileuploader log contract unchanged', async (t) => {
|
||||||
const directory = createTempDirectory(t, 'mhu-upload-log-contract-');
|
const directory = createTempDirectory(t, 'mhu-upload-log-contract-');
|
||||||
const userDataPath = path.join(directory, 'user-data');
|
const userDataPath = path.join(directory, 'user-data');
|
||||||
|
|||||||
Reference in New Issue
Block a user