Skip to content

Fix FileTraceListener encoding to UTF-8 and fix DisposeAsync drain order#234

Merged
Aaronontheweb merged 2 commits into
devfrom
fix/filereceivelistener-encoding
May 29, 2026
Merged

Fix FileTraceListener encoding to UTF-8 and fix DisposeAsync drain order#234
Aaronontheweb merged 2 commits into
devfrom
fix/filereceivelistener-encoding

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Owner

Problem

was using StreamWriter(filePath, append: false) with Encoding.Default (system codepage). When trace output contained Unicode characters (e.g., ➭ U+27AD), the consumer task would throw ArgumentException: Unable to translate Unicode character at index X to specified code page, crashing the consumer. With an unbounded channel, this would fill up and OOM-kill the pod.

This is related to the earlier fix in PR #208, which addressed the same root cause (encoding mismatch) for TUI rendering (Console.Out). This fix handles the trace logging path which was missed.

Changes

FileTraceListener.cs

  • StreamWriter now explicitly uses Encoding.UTF8 — same as AnsiTerminal does for Console.Out
  • DisposeAsync now sets _disposed = true after waiting for the consumer to drain the channel. Previously, _disposed=true was set before the drain, causing the consumer to skip all events via IsEnabled() — the file would be empty even if the consumer didn't crash.

FileTraceListenerTests.cs (new)

  • FileTraceListener_uses_UTF8_encoding_for_file_writers — writes Unicode content, disposes, verifies file contains all characters
  • FileTraceListener_consumer_handles_unicode_under_stress — pushes 100 Unicode events, verifies all 100 lines written (consumer didn't fault)
  • StreamWriter_in_FileTraceListener_is_UTF8_not_Default — verifies dispose doesn't throw even with pure Unicode content

Testing

  • All 1141 existing tests pass
  • 3 new regression tests added

- FileTraceListener StreamWriter now uses Encoding.UTF8 instead of
  Encoding.Default, preventing ArgumentException on Unicode characters
  (e.g., ➭ U+27AD). This was causing consumer task faults, channel
  fill, and OOM-killed pods when trace output contained non-ASCII.

- DisposeAsync now sets _disposed AFTER waiting for consumer to drain.
  Previously _disposed=true was set before the drain, causing the
  consumer to see disposed=true and skip all events — leaving the
  file empty even when the fix was applied.

- Add regression tests verifying UTF-8 encoding and consumer resilience
  under Unicode stress.
- Use UTF-8 without a BOM (UTF8Encoding(false)) so trace files don't get a
  stray EF BB BF prefix that confuses line-oriented log tooling.
- Add a single-shot _disposeStarted guard so concurrent/repeated Dispose() and
  DisposeAsync() can't call _channel.Writer.Complete() twice (which throws).
- Apply the same drain-order fix to the synchronous Dispose() path: it set
  _disposed=true and cancelled the consumer token BEFORE draining, so buffered
  events were skipped and the file came out empty. _disposed/_cts.Cancel() now
  happen after the drain, matching DisposeAsync.
- Strengthen tests: sync-dispose now asserts flushed content, plus new no-BOM
  and redundant-disposal regression tests; temp files are cleaned up.
@Aaronontheweb

Copy link
Copy Markdown
Owner Author

Pushed follow-ups (commit 2ea0624) addressing three things found in review:

  1. UTF-8 BOM: Encoding.UTF8 emits a preamble, so every trace file would start with a stray EF BB BF that line-oriented tools (tail/grep/log shippers) surface as garbage on line 1. Switched to UTF8Encoding(encoderShouldEmitUTF8Identifier: false). Added a no-BOM regression test.
  2. Sync Dispose() still had the original drain bug — the PR only fixed DisposeAsync. Dispose() set _disposed = true and cancelled the consumer token before the drain, so buffered events were skipped and the file came out empty. Moved both to after the drain; strengthened the former no-op test Bump docfx from 2.78.3 to 2.78.4 #3 to assert flushed content via the sync path.
  3. Double-dispose hazard introduced by the reorder: deferring _disposed defeated the if (_disposed) return re-entrancy guard, so concurrent/mixed disposal could call _channel.Writer.Complete() twice (throws). Added a single-shot Interlocked _disposeStarted guard + a redundant-disposal test.

Full suite green (1143). Can't self-approve as author — flagging as ready.

@Aaronontheweb Aaronontheweb enabled auto-merge (squash) May 29, 2026 02:26
@Aaronontheweb Aaronontheweb merged commit 9b27bb7 into dev May 29, 2026
13 checks passed
@Aaronontheweb Aaronontheweb deleted the fix/filereceivelistener-encoding branch May 29, 2026 02:27
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