171 lines
5.4 KiB
C#
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 { }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|