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 _queue = new ConcurrentQueue(); 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(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 { } } } } } }