HanvonAttendanceService/LogService.cs

171 lines
5.4 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace HanvonF710XAttendanceService
{
internal static class LogService
{
private sealed class LogItem
{
public string FilePath { get; }
public string Line { get; }
public LogItem(string filePath, string line)
{
FilePath = filePath;
Line = line;
}
}
private static readonly ConcurrentQueue<LogItem> _queue = new ConcurrentQueue<LogItem>();
private static readonly object _startLock = new object();
private static CancellationTokenSource _cts;
private static Task _worker;
private static volatile bool _started;
// One global cross-process mutex to serialize file writes across multiple services.
private static readonly Mutex _globalMutex = new Mutex(false, @"Global\HanvonF710XAttendanceService_LogMutex");
public static void Start()
{
lock (_startLock)
{
if (_started) return;
_started = true;
_cts = new CancellationTokenSource();
_worker = Task.Run(() => WorkerLoop(_cts.Token));
}
}
public static void StopAndFlush(TimeSpan maxWait)
{
lock (_startLock)
{
if (!_started) return;
_cts.Cancel();
}
try { _worker?.Wait(maxWait); } catch { }
// Best-effort final flush
try { FlushOnce(maxItems: 5000, allowWait: true); } catch { }
}
public static void EnqueueLine(string filePath, string line)
{
if (string.IsNullOrWhiteSpace(filePath)) return;
if (line == null) line = "";
if (!_started)
{
// If service forgot to start the logger, still try to start it lazily.
Start();
}
_queue.Enqueue(new LogItem(filePath, line));
}
private static async Task WorkerLoop(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
FlushOnce(maxItems: 500, allowWait: false);
}
catch
{
// swallow; keep loop alive
}
try
{
await Task.Delay(1000, token).ConfigureAwait(false);
}
catch
{
// ignore cancellation timing
}
}
}
private static void FlushOnce(int maxItems, bool allowWait)
{
if (_queue.IsEmpty) return;
var drained = new List<LogItem>(maxItems);
while (drained.Count < maxItems && _queue.TryDequeue(out var item))
{
drained.Add(item);
}
if (drained.Count == 0) return;
bool mutexTaken = false;
try
{
// Avoid blocking service operations; only background worker waits.
int waitMs = allowWait ? 2000 : 0;
mutexTaken = _globalMutex.WaitOne(waitMs);
if (!mutexTaken)
{
// Couldn't acquire lock; put back and try later.
for (int i = drained.Count - 1; i >= 0; i--)
{
_queue.Enqueue(drained[i]);
}
return;
}
foreach (var grp in drained.GroupBy(d => d.FilePath, StringComparer.OrdinalIgnoreCase))
{
string filePath = grp.Key;
try
{
var dir = Path.GetDirectoryName(filePath);
if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
// Open with FileShare.ReadWrite so other processes can read while we write.
using (var fs = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
using (var sw = new StreamWriter(fs, Encoding.UTF8))
{
foreach (var li in grp)
{
sw.WriteLine(li.Line);
}
}
}
catch (IOException)
{
// If file is temporarily locked, re-enqueue and try next flush.
foreach (var li in grp)
{
_queue.Enqueue(li);
}
}
catch
{
// Drop lines on unexpected errors (best-effort logging).
}
}
}
finally
{
if (mutexTaken)
{
try { _globalMutex.ReleaseMutex(); } catch { }
}
}
}
}
}