Skip to content

fix(logs): drop stale-stat gate that hid appends on Windows#43

Merged
Rinse12 merged 1 commit into
masterfrom
fix/logs-follow-windows-stat-stale
May 20, 2026
Merged

fix(logs): drop stale-stat gate that hid appends on Windows#43
Rinse12 merged 1 commit into
masterfrom
fix/logs-follow-windows-stat-stale

Conversation

@Rinse12
Copy link
Copy Markdown
Member

@Rinse12 Rinse12 commented May 20, 2026

Summary

  • The earlier fix (fix(logs): use userspace polling for -f follow mode #41) replaced fs.watchFile with userspace polling but kept the fsPromise.stat(file).size > position gate. On Windows + NTFS, stat() returns a stale size for a short window after another process appends, so the gate stays false and the read never fires. Linux/macOS update size promptly, masking the bug — which is why CI passed there but the windows-latest job continued to fail at test/cli/logs.test.ts:462 (expected … to contain 'APPENDED_LINE').
  • This PR drops the gate and reads directly from position in a loop until read() returns 0 bytes. read() sees the true file end at syscall time and doesn't depend on metadata being fresh.
  • Re-opens bitsocial logs -f doesn't see appended lines on Windows #40 (its previous closure was premature).

Test plan

  • npm run build && npm run build:test
  • npx vitest run test/cli/logs.test.ts — all 29 tests pass locally on Linux (including the continues watching old file if no new file appears regression case).
  • CI windows-latest job passes (the actual regression we're targeting).

Summary by CodeRabbit

  • Bug Fixes
    • Improved the efficiency of bitsocial logs --follow command by optimizing how log files are monitored and tailed. The command now reads log updates more efficiently and better handles incomplete log lines when filtering or streaming output.

Review Change Stack

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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

Updated the follow-mode log tailing in bitsocial logs to read new bytes directly via repeated fd.read calls from the current file position, replacing the stat().size polling gate. The logic builds UTF-8 text chunks, splits incomplete trailing lines into a pendingBuffer, and conditionally streams either raw text or newly parsed/filtered log entries.

Changes

Follow-mode tailing buffer refactor

Layer / File(s) Summary
Follow-mode incremental buffer reading and streaming logic
src/cli/commands/logs.ts
readNewData removes the stat().size gate and single-shot read, replacing them with a loop of fd.read calls from the current position. New bytes are decoded into UTF-8 text chunks; a pendingBuffer holds incomplete trailing lines split on the last newline. Raw text streams when no filters are active; newly completed log entries are parsed and filtered when --since, --until, or stdout/stderr filters are in use. Error swallowing for rotation/deletion and finally block closure are retained.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related issues

  • #40: Both changes address how new appended bytes are detected and read in the follow-mode tailing flow, replacing stat()/watchFile gating with more direct position-based reading strategies.

Possibly related PRs

  • bitsocialnet/bitsocial-cli#41: Both PRs modify src/cli/commands/logs.ts follow-mode tailing to replace stat().size polling with position-based incremental reads, including changes to buffering and file-switch logic.

Poem

🐰 Hops through logs with glee,
No stat() gates to pass, you see,
Read by position, byte by byte,
Chunks flow true, UTF-8 so bright,
Filters dance on pending lines,
Tailing works in newfound times! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: removing a stat-based gate that was preventing log appends from being read on Windows, which is the core fix in this PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/logs-follow-windows-stat-stale

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/cli/commands/logs.ts

Oops! Something went wrong! :(

ESLint: 8.27.0

Error: ESLint configuration in --config » eslint-config-oclif is invalid:

  • Unexpected top-level property "__esModule".

Referenced from: /.eslintrc
at ConfigValidator.validateConfigSchema (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2156:19)
at ConfigArrayFactory._normalizeConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2998:19)
at ConfigArrayFactory._loadConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2963:21)
at ConfigArrayFactory._loadExtendedShareableConfig (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3264:21)
at ConfigArrayFactory._loadExtends (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3135:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3074:25)
at _normalizeObjectConfigDataBody.next ()
at ConfigArrayFactory._normalizeObjectConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3019:20)
at _normalizeObjectConfigData.next ()
at ConfigArrayFactory.loadFile (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2829:16)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/cli/commands/logs.ts (1)

211-218: 💤 Low value

Consider using TextDecoder for correct multi-byte UTF-8 handling.

If a multi-byte UTF-8 character (emoji, non-ASCII path, etc.) spans the 64KB buffer boundary, calling .toString("utf-8") independently on each chunk produces replacement characters at the split. Using TextDecoder with streaming mode handles partial sequences correctly.

♻️ Suggested improvement
+        const decoder = new TextDecoder("utf-8", { fatal: false });
         const readNewData = async () => {
             let fd: fsPromise.FileHandle | undefined;
             try {
                 fd = await fsPromise.open(currentLogFile, "r");
                 let chunk = "";
                 while (true) {
                     const { bytesRead } = await fd.read(readBuf, 0, readBuf.length, position);
                     if (bytesRead === 0) break;
                     position += bytesRead;
-                    chunk += readBuf.subarray(0, bytesRead).toString("utf-8");
+                    chunk += decoder.decode(readBuf.subarray(0, bytesRead), { stream: true });
                     if (bytesRead < readBuf.length) break;
                 }
+                chunk += decoder.decode(); // flush any remaining partial sequence
                 if (!chunk) return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/commands/logs.ts` around lines 211 - 218, The current read loop
builds string chunks by calling readBuf.subarray(...).toString("utf-8") which
can corrupt multi-byte UTF‑8 sequences split across buffer boundaries; replace
the chunk accumulation with a TextDecoder in streaming mode (create new
TextDecoder("utf-8") before the loop and call decoder.decode(readBuf.subarray(0,
bytesRead), { stream: true }) for each read, then after the loop append
decoder.decode(undefined) to flush any remainder), updating the usage around
variables fd, readBuf, position, chunk so the final text is correctly decoded
without replacement characters.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/cli/commands/logs.ts`:
- Around line 211-218: The current read loop builds string chunks by calling
readBuf.subarray(...).toString("utf-8") which can corrupt multi-byte UTF‑8
sequences split across buffer boundaries; replace the chunk accumulation with a
TextDecoder in streaming mode (create new TextDecoder("utf-8") before the loop
and call decoder.decode(readBuf.subarray(0, bytesRead), { stream: true }) for
each read, then after the loop append decoder.decode(undefined) to flush any
remainder), updating the usage around variables fd, readBuf, position, chunk so
the final text is correctly decoded without replacement characters.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8ca99141-a111-41be-adec-985d736ccce9

📥 Commits

Reviewing files that changed from the base of the PR and between 33a87f6 and 844eaac.

📒 Files selected for processing (1)
  • src/cli/commands/logs.ts

@Rinse12 Rinse12 merged commit a5fa473 into master May 20, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant