Skip to content

Commit 844eaac

Browse files
committed
fix(logs): drop stale-stat gate, read directly from position
The previous fix (#41) switched `-f` follow mode from fs.watchFile to userspace polling, but kept the `fsPromise.stat(file).size > position` gate before reading. On Windows + NTFS, stat() returns a stale size for a short window after another process appends to the file, so the gate evaluates false and the read never fires. Linux/macOS happen to update the size promptly, masking the bug. Drop the gate and read directly from `position` in a loop until read() returns 0 bytes. The read() syscall sees the true file end at call time and doesn't depend on metadata being fresh. Re-opens #40.
1 parent 33a87f6 commit 844eaac

1 file changed

Lines changed: 36 additions & 27 deletions

File tree

src/cli/commands/logs.ts

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -198,40 +198,49 @@ export default class Logs extends Command {
198198
let position = stat.size;
199199
let pendingBuffer = "";
200200

201-
// Watch for new data using polling (works across filesystems including Docker volumes)
201+
// Watch for new data by reading directly from `position`. We intentionally do
202+
// NOT gate on fsPromise.stat().size — on Windows + NTFS, stat() returns a stale
203+
// size for a short window after another process appends, which causes the gate
204+
// to miss new bytes. read() sees the true file end at syscall time.
205+
const READ_BUF_SIZE = 64 * 1024;
206+
const readBuf = Buffer.alloc(READ_BUF_SIZE);
202207
const readNewData = async () => {
208+
let fd: fsPromise.FileHandle | undefined;
203209
try {
204-
const currentStat = await fsPromise.stat(currentLogFile);
205-
if (currentStat.size > position) {
206-
const fd = await fsPromise.open(currentLogFile, "r");
207-
const buf = new Uint8Array(currentStat.size - position);
208-
const { bytesRead } = await fd.read(buf, 0, buf.length, position);
209-
await fd.close();
210+
fd = await fsPromise.open(currentLogFile, "r");
211+
let chunk = "";
212+
while (true) {
213+
const { bytesRead } = await fd.read(readBuf, 0, readBuf.length, position);
214+
if (bytesRead === 0) break;
210215
position += bytesRead;
216+
chunk += readBuf.subarray(0, bytesRead).toString("utf-8");
217+
if (bytesRead < readBuf.length) break;
218+
}
219+
if (!chunk) return;
211220

212-
const chunk = pendingBuffer + new TextDecoder().decode(buf.subarray(0, bytesRead));
213-
// Split into complete lines; keep any incomplete trailing line in the buffer
214-
const lastNewline = chunk.lastIndexOf("\n");
215-
if (lastNewline === -1) {
216-
pendingBuffer = chunk;
217-
return;
218-
}
219-
pendingBuffer = chunk.slice(lastNewline + 1);
220-
const completeText = chunk.slice(0, lastNewline + 1);
221-
222-
if (!since && !until && !streamFilter) {
223-
// No filtering — pass through directly
224-
process.stdout.write(completeText);
225-
} else {
226-
const newEntries = this._parseLogEntries(completeText.replace(/\n$/, ""));
227-
const filteredNew = this._filterEntries(newEntries, since, until);
228-
const streamFilteredNew = streamFilter ? this._filterByStream(filteredNew, streamFilter) : filteredNew;
229-
const output = streamFilteredNew.map((e) => e.lines.join("\n")).join("\n");
230-
if (output) process.stdout.write(output + "\n");
231-
}
221+
const combined = pendingBuffer + chunk;
222+
const lastNewline = combined.lastIndexOf("\n");
223+
if (lastNewline === -1) {
224+
pendingBuffer = combined;
225+
return;
226+
}
227+
pendingBuffer = combined.slice(lastNewline + 1);
228+
const completeText = combined.slice(0, lastNewline + 1);
229+
230+
if (!since && !until && !streamFilter) {
231+
// No filtering — pass through directly
232+
process.stdout.write(completeText);
233+
} else {
234+
const newEntries = this._parseLogEntries(completeText.replace(/\n$/, ""));
235+
const filteredNew = this._filterEntries(newEntries, since, until);
236+
const streamFilteredNew = streamFilter ? this._filterByStream(filteredNew, streamFilter) : filteredNew;
237+
const output = streamFilteredNew.map((e) => e.lines.join("\n")).join("\n");
238+
if (output) process.stdout.write(output + "\n");
232239
}
233240
} catch {
234241
// File may have been rotated or deleted
242+
} finally {
243+
if (fd) await fd.close().catch(() => {});
235244
}
236245
};
237246

0 commit comments

Comments
 (0)