Problem
The library's live log viewer only works in WPF. BAUERGROUP.Shared.Desktop targets net10.0-windows;net8.0-windows with UseWPF/UseWindowsForms (src/BAUERGROUP.Shared.Desktop/BAUERGROUP.Shared.Desktop.csproj:4-6). An Avalonia application on Linux therefore cannot use WPFToolbox.LogMessageReceiverWindow (src/BAUERGROUP.Shared.Desktop/WPFToolbox.cs:19) and has to build its own viewer.
A straight port would also bring along problems in the current implementation:
-
Two unrelated mechanisms. The window (src/BAUERGROUP.Shared.Desktop/Logging/LogMessageReceiver.xaml:13) hosts LogMessageReceiverControl, which receives log lines over UDP loopback through BGLoggerNetworkListener (Logging/LogMessageReceiverControl.xaml.cs:39). Logging/TextBoxTraceListener.cs is a separate TraceListener that nothing in the library uses.
-
Global side effects of the UDP listener. BGLoggerNetworkListener sets BGLogger.Configuration.Network = true (src/BAUERGROUP.Shared.Core/Logging/BGLoggerNetworkListener.cs:43-44) and never sets it back. From then on, every log line is also sent to udp://127.0.0.1:9898 (the default NetworkPort, BGLoggerConfiguration.cs:44, :533, :560) until the process ends. Dispose only cancels and disposes the CancellationTokenSource (:47-52), and the loop checks the token only between blocking UdpClient.Receive calls (:28-30). The socket is therefore released only after the next datagram arrives.
-
Buffer is either unbounded or loses history. Both viewers append with Text += (Logging/TextBoxTraceListener.cs:44, Logging/LogMessageReceiverControl.xaml.cs:53), which copies the whole text on every line. TextBoxTraceListener never trims. LogMessageReceiverControl clears all text once it passes 1000 lines (Logging/LogMessageReceiverControl.xaml.cs:57-58) instead of dropping only the oldest lines.
-
The producer is blocked. TextBoxTraceListener uses Dispatcher.Invoke (Logging/TextBoxTraceListener.cs:42), so the thread that logs waits for the UI thread.
-
Relies on window placement. The window is Topmost="True" with Left="5" Top="5" (Logging/LogMessageReceiver.xaml:11). Native Wayland does not honour that. In Avalonia.Wayland 12.1.2, Position, Activate() and SetTopmost() do nothing (WindowImplBase.cs#L67-L72), neither does Move (WindowImpl.cs#L310-L311), and Window.Activate() only forwards to the platform (WindowBase.cs#L142-L145).
-
The Trace route is off by default. A viewer that only adds a TraceListener shows no BGLogger output, because the TRACE target is disabled by default (src/BAUERGROUP.Shared.Core/Logging/BGLoggerConfiguration.cs:62, property at :322). I checked this with a small console program against BAUERGROUP.Shared.Core 3.0.8 that registers a TraceListener:
Configuration.Trace default = False
lines captured after default: 0
lines captured after Trace = true: 1
These files have not changed between v3.0.8 and origin/main (git diff --stat v3.0.8 origin/main -- src/BAUERGROUP.Shared.Desktop src/BAUERGROUP.Shared.Core/Logging prints nothing).
Proposal
Build an Avalonia log viewer as a control first. It should work the same on Windows, X11 and native Wayland.
- Embeddable control (e.g.
LogViewer). The host places it in its own window: a panel, tab, splitter or overlay. This form does not depend on window position, stacking order or activation, so it behaves the same on every backend.
- Optional window wrapper (e.g.
LogViewerWindow) that hosts the same control. It is shown with an owner (Show(owner), which maps to xdg_toplevel.set_parent on Wayland, WindowImpl.cs#L235-L239). Topmost and Position are hints at most. The wrapper must stay usable when they are ignored, and must not rely on Activate() to bring an existing window back.
- Log source owned by the control. Attach on
AttachedToVisualTree and detach on DetachedFromVisualTree. Both must be safe to call twice, and nothing may be left registered after detach. Two options:
- (a) A
TraceListener that sets BGLogger.Configuration.Trace = true while attached and restores the previous value on detach. This is simple, but Trace is process-wide: all other registered listeners receive the lines too, and several viewers need reference counting.
- (b) Preferred: a small in-process sink API in Core, e.g.
IDisposable BGLogger.Configuration.AddLiveSink(Action<LogLine>). It adds an NLog target while at least one sink is registered, has no Trace side effects, and can pass the level along for colouring or filtering.
- Not UDP: inside the same process, a fixed port and a global
Network switch add nothing.
- Bounded buffer. Use a ring buffer of N lines (configurable, default e.g. 5000) that drops the oldest lines. Render only what the buffer holds, e.g. a virtualized list, or a text view rebuilt from the buffer at a throttled rate. Never re-assign the entire accumulated text for each line.
- Threading. The logging thread only enqueues (non-blocking, never
Dispatcher.Invoke). A UI timer (e.g. every 100-250 ms) empties the queue in batches, so a burst of lines causes one UI update instead of one Post per line.
- Autoscroll to the newest line by default, as v1 does (
Logging/LogMessageReceiverControl.xaml.cs:60). Pausing it while the user has scrolled up is optional.
- Headless tests (Avalonia.Headless) for: nothing left registered after detach, the buffer limit, and batched draining.
The UI-independent parts (the sink API and the ring buffer) belong in BAUERGROUP.Shared.Core. The existing WPF LogMessageReceiverControl can then use them too, replacing the UDP listener and fixing its buffer.
Open decision: packaging
| Option |
Pros |
Cons |
New BAUERGROUP.Shared.Avalonia package (net8.0;net10.0, no -windows) |
Linux consumers get no WPF/WinForms dependency; WPF consumers get no Avalonia dependency; TFMs stay simple |
One more package to version and release; CI has to build and test it on a non-Windows TFM |
Inside BAUERGROUP.Shared.Desktop |
No new package |
Desktop is *-windows with UseWPF/UseWindowsForms (csproj:4-6), so a Linux net10.0 build cannot reference it without multi-targeting and conditional compilation; every WPF consumer would pull in Avalonia |
Context
bgIndustrialAutomation Client (bauer-group/OT-AutomationClient) moved from WPF to Avalonia 12 and targets Windows and Linux. It now runs natively on Wayland through Avalonia.Wayland 12.1.2 where the compositor supports it, and falls back to X11 otherwise (bauer-group/OT-AutomationClient@0c8529b). As a workaround the application rebuilt the F12 viewer in src/BAUERGROUP.bgIndustrialAutomation.Client.App/Views/LogWindow.axaml(.cs) (application references below are at its main, commit 3f6fb39). Its remarks say the code should move into a BAUERGROUP.Shared.Avalonia package once it has proven itself (LogWindow.axaml.cs:15-20). The rebuild shows the gaps described above:
- It is a topmost window at
Position="5,5" (LogWindow.axaml:9-11). Pressing F12 again calls _logWindow.Activate() (App.axaml.cs:299-303), which does nothing on Wayland.
- Its
StringBuilder _buffer (LogWindow.axaml.cs:40) grows for as long as the window stays open, e.g. a whole shift. On every line, Text is replaced with the whole buffer (:87-92), so each new line costs more than the last.
- It adds a
TraceListener (:55-58) but never enables BGLogger.Configuration.Trace, so under library defaults no BGLogger lines reach it (see the check above). That is a defect in the application. It is mentioned here because a control that owns its log source rules out this mistake.
The application uses BAUERGROUP.Shared.Core 3.0.8 and does not reference BAUERGROUP.Shared.Desktop. Library references are to origin/main at 02325dd.
Problem
The library's live log viewer only works in WPF.
BAUERGROUP.Shared.Desktoptargetsnet10.0-windows;net8.0-windowswithUseWPF/UseWindowsForms(src/BAUERGROUP.Shared.Desktop/BAUERGROUP.Shared.Desktop.csproj:4-6). An Avalonia application on Linux therefore cannot useWPFToolbox.LogMessageReceiverWindow(src/BAUERGROUP.Shared.Desktop/WPFToolbox.cs:19) and has to build its own viewer.A straight port would also bring along problems in the current implementation:
Two unrelated mechanisms. The window (
src/BAUERGROUP.Shared.Desktop/Logging/LogMessageReceiver.xaml:13) hostsLogMessageReceiverControl, which receives log lines over UDP loopback throughBGLoggerNetworkListener(Logging/LogMessageReceiverControl.xaml.cs:39).Logging/TextBoxTraceListener.csis a separateTraceListenerthat nothing in the library uses.Global side effects of the UDP listener.
BGLoggerNetworkListenersetsBGLogger.Configuration.Network = true(src/BAUERGROUP.Shared.Core/Logging/BGLoggerNetworkListener.cs:43-44) and never sets it back. From then on, every log line is also sent toudp://127.0.0.1:9898(the defaultNetworkPort,BGLoggerConfiguration.cs:44,:533,:560) until the process ends.Disposeonly cancels and disposes theCancellationTokenSource(:47-52), and the loop checks the token only between blockingUdpClient.Receivecalls (:28-30). The socket is therefore released only after the next datagram arrives.Buffer is either unbounded or loses history. Both viewers append with
Text +=(Logging/TextBoxTraceListener.cs:44,Logging/LogMessageReceiverControl.xaml.cs:53), which copies the whole text on every line.TextBoxTraceListenernever trims.LogMessageReceiverControlclears all text once it passes 1000 lines (Logging/LogMessageReceiverControl.xaml.cs:57-58) instead of dropping only the oldest lines.The producer is blocked.
TextBoxTraceListenerusesDispatcher.Invoke(Logging/TextBoxTraceListener.cs:42), so the thread that logs waits for the UI thread.Relies on window placement. The window is
Topmost="True"withLeft="5" Top="5"(Logging/LogMessageReceiver.xaml:11). Native Wayland does not honour that. In Avalonia.Wayland 12.1.2,Position,Activate()andSetTopmost()do nothing (WindowImplBase.cs#L67-L72), neither doesMove(WindowImpl.cs#L310-L311), andWindow.Activate()only forwards to the platform (WindowBase.cs#L142-L145).The Trace route is off by default. A viewer that only adds a
TraceListenershows noBGLoggeroutput, because the TRACE target is disabled by default (src/BAUERGROUP.Shared.Core/Logging/BGLoggerConfiguration.cs:62, property at:322). I checked this with a small console program againstBAUERGROUP.Shared.Core3.0.8 that registers aTraceListener:These files have not changed between
v3.0.8andorigin/main(git diff --stat v3.0.8 origin/main -- src/BAUERGROUP.Shared.Desktop src/BAUERGROUP.Shared.Core/Loggingprints nothing).Proposal
Build an Avalonia log viewer as a control first. It should work the same on Windows, X11 and native Wayland.
LogViewer). The host places it in its own window: a panel, tab, splitter or overlay. This form does not depend on window position, stacking order or activation, so it behaves the same on every backend.LogViewerWindow) that hosts the same control. It is shown with an owner (Show(owner), which maps toxdg_toplevel.set_parenton Wayland,WindowImpl.cs#L235-L239).TopmostandPositionare hints at most. The wrapper must stay usable when they are ignored, and must not rely onActivate()to bring an existing window back.AttachedToVisualTreeand detach onDetachedFromVisualTree. Both must be safe to call twice, and nothing may be left registered after detach. Two options:TraceListenerthat setsBGLogger.Configuration.Trace = truewhile attached and restores the previous value on detach. This is simple, but Trace is process-wide: all other registered listeners receive the lines too, and several viewers need reference counting.IDisposable BGLogger.Configuration.AddLiveSink(Action<LogLine>). It adds an NLog target while at least one sink is registered, has no Trace side effects, and can pass the level along for colouring or filtering.Networkswitch add nothing.Dispatcher.Invoke). A UI timer (e.g. every 100-250 ms) empties the queue in batches, so a burst of lines causes one UI update instead of onePostper line.Logging/LogMessageReceiverControl.xaml.cs:60). Pausing it while the user has scrolled up is optional.The UI-independent parts (the sink API and the ring buffer) belong in
BAUERGROUP.Shared.Core. The existing WPFLogMessageReceiverControlcan then use them too, replacing the UDP listener and fixing its buffer.Open decision: packaging
BAUERGROUP.Shared.Avaloniapackage (net8.0;net10.0, no-windows)BAUERGROUP.Shared.Desktop*-windowswithUseWPF/UseWindowsForms(csproj:4-6), so a Linuxnet10.0build cannot reference it without multi-targeting and conditional compilation; every WPF consumer would pull in AvaloniaContext
bgIndustrialAutomation Client (bauer-group/OT-AutomationClient) moved from WPF to Avalonia 12 and targets Windows and Linux. It now runs natively on Wayland through Avalonia.Wayland 12.1.2 where the compositor supports it, and falls back to X11 otherwise (bauer-group/OT-AutomationClient@0c8529b). As a workaround the application rebuilt the F12 viewer in
src/BAUERGROUP.bgIndustrialAutomation.Client.App/Views/LogWindow.axaml(.cs)(application references below are at itsmain, commit3f6fb39). Its remarks say the code should move into aBAUERGROUP.Shared.Avaloniapackage once it has proven itself (LogWindow.axaml.cs:15-20). The rebuild shows the gaps described above:Position="5,5"(LogWindow.axaml:9-11). Pressing F12 again calls_logWindow.Activate()(App.axaml.cs:299-303), which does nothing on Wayland.StringBuilder _buffer(LogWindow.axaml.cs:40) grows for as long as the window stays open, e.g. a whole shift. On every line,Textis replaced with the whole buffer (:87-92), so each new line costs more than the last.TraceListener(:55-58) but never enablesBGLogger.Configuration.Trace, so under library defaults noBGLoggerlines reach it (see the check above). That is a defect in the application. It is mentioned here because a control that owns its log source rules out this mistake.The application uses
BAUERGROUP.Shared.Core3.0.8 and does not referenceBAUERGROUP.Shared.Desktop. Library references are toorigin/mainat02325dd.