diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 3ff8696d7e..2086a8eb88 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -42,6 +42,7 @@ + diff --git a/OpenRA.Game/Support/Log.cs b/OpenRA.Game/Support/Log.cs index df21610a85..2488939514 100644 --- a/OpenRA.Game/Support/Log.cs +++ b/OpenRA.Game/Support/Log.cs @@ -10,8 +10,11 @@ #endregion using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Channels; namespace OpenRA { @@ -22,90 +25,154 @@ namespace OpenRA public TextWriter Writer; } + readonly struct ChannelData + { + public readonly string Channel; + public readonly string Text; + + public ChannelData(string channel, string text) + { + Text = text; + Channel = channel; + } + } + public static class Log { - static readonly Dictionary Channels = new Dictionary(); + const int CreateLogFileMaxRetryCount = 128; - static IEnumerable FilenamesForChannel(string channelName, string baseFilename) + static readonly ConcurrentDictionary Channels = new ConcurrentDictionary(); + static readonly Channel Channel; + static readonly ChannelWriter ChannelWriter; + static readonly CancellationTokenSource CancellationToken = new CancellationTokenSource(); + + static readonly TimeSpan FlushInterval = TimeSpan.FromSeconds(5); + static readonly Timer Timer; + static readonly Thread Thread; + + static Log() + { + Channel = System.Threading.Channels.Channel.CreateUnbounded(); + ChannelWriter = Channel.Writer; + + Thread = new Thread(DoWork); + var cancellationTokenToken = CancellationToken.Token; + Thread.Start(cancellationTokenToken); + + Timer = new Timer(FlushToDisk, cancellationTokenToken, FlushInterval, Timeout.InfiniteTimeSpan); + } + + static void FlushToDisk(object state) + { + FlushToDisk(); + + var token = (CancellationToken)state; + if (!token.IsCancellationRequested) + Timer.Change(FlushInterval, Timeout.InfiniteTimeSpan); + } + + static void FlushToDisk() + { + foreach (var (_, channel) in Channels) + channel.Writer?.Flush(); + } + + static void DoWork(object obj) + { + var token = (CancellationToken)obj; + var reader = Channel.Reader; + + while (!token.IsCancellationRequested) + if (reader.TryRead(out var item)) + WriteValue(item); + + while (reader.TryRead(out var item)) + WriteValue(item); + + FlushToDisk(); + } + + static void WriteValue(ChannelData item) + { + var channel = GetChannel(item.Channel); + var writer = channel.Writer; + if (writer == null) + return; + + if (!channel.IsTimestamped) + writer.WriteLine(item.Text); + else + { + var timestamp = DateTime.Now.ToString(Game.Settings.Server.TimestampFormat); + writer.WriteLine("[{0}] {1}", timestamp, item.Text); + } + } + + static IEnumerable FilenamesForChannel(string baseFilename) { var path = Platform.SupportDir + "Logs"; Directory.CreateDirectory(path); - for (var i = 0; ; i++) + for (var i = 0; i < CreateLogFileMaxRetryCount; i++) yield return Path.Combine(path, i > 0 ? "{0}.{1}".F(baseFilename, i) : baseFilename); + + throw new ApplicationException("Error creating log file \"{0}\"".F(baseFilename)); } - public static ChannelInfo Channel(string channelName) + static ChannelInfo GetChannel(string channelName) { - ChannelInfo info; - lock (Channels) - if (!Channels.TryGetValue(channelName, out info)) - throw new ArgumentException("Tried logging to non-existent channel " + channelName, nameof(channelName)); + if (!Channels.TryGetValue(channelName, out var info)) + throw new ArgumentException("Tried logging to non-existent channel " + channelName, nameof(channelName)); return info; } public static void AddChannel(string channelName, string baseFilename, bool isTimestamped = false) { - lock (Channels) - { - if (Channels.ContainsKey(channelName)) return; + if (Channels.ContainsKey(channelName)) + return; - if (string.IsNullOrEmpty(baseFilename)) + if (string.IsNullOrEmpty(baseFilename)) + { + Channels.TryAdd(channelName, default); + return; + } + + foreach (var filename in FilenamesForChannel(baseFilename)) + { + try { - Channels.Add(channelName, default(ChannelInfo)); + var writer = File.CreateText(filename); + writer.AutoFlush = false; + + Channels.TryAdd(channelName, + new ChannelInfo + { + Filename = filename, + IsTimestamped = isTimestamped, + Writer = TextWriter.Synchronized(writer) + }); + return; } - - foreach (var filename in FilenamesForChannel(channelName, baseFilename)) - try - { - var writer = File.CreateText(filename); - writer.AutoFlush = true; - - Channels.Add(channelName, - new ChannelInfo - { - Filename = filename, - IsTimestamped = isTimestamped, - Writer = TextWriter.Synchronized(writer) - }); - - return; - } - catch (IOException) { } + catch (IOException) { } } } public static void Write(string channelName, string value) { - var channel = Channel(channelName); - var writer = channel.Writer; - if (writer == null) - return; - - if (!channel.IsTimestamped) - writer.WriteLine(value); - else - { - var timestamp = DateTime.Now.ToString(Game.Settings.Server.TimestampFormat); - writer.WriteLine("[{0}] {1}", timestamp, value); - } + ChannelWriter.TryWrite(new ChannelData(channelName, value)); } public static void Write(string channelName, string format, params object[] args) { - var channel = Channel(channelName); - if (channel.Writer == null) - return; + ChannelWriter.TryWrite(new ChannelData(channelName, format.F(args))); + } - if (!channel.IsTimestamped) - channel.Writer.WriteLine(format, args); - else - { - var timestamp = DateTime.Now.ToString(Game.Settings.Server.TimestampFormat); - channel.Writer.WriteLine("[{0}] {1}", timestamp, format.F(args)); - } + public static void Dispose() + { + CancellationToken.Cancel(); + Timer.Dispose(); } } } diff --git a/OpenRA.Launcher/Program.cs b/OpenRA.Launcher/Program.cs index 7bc7ee509a..fd26346c8b 100644 --- a/OpenRA.Launcher/Program.cs +++ b/OpenRA.Launcher/Program.cs @@ -34,6 +34,10 @@ namespace OpenRA.Launcher ExceptionHandler.HandleFatalError(e); return (int)RunStatus.Error; } + finally + { + Log.Dispose(); + } } } } diff --git a/OpenRA.Server/Program.cs b/OpenRA.Server/Program.cs index c19b759dc3..1bc39bf0c6 100644 --- a/OpenRA.Server/Program.cs +++ b/OpenRA.Server/Program.cs @@ -22,6 +22,18 @@ namespace OpenRA.Server class Program { static void Main(string[] args) + { + try + { + Run(args); + } + finally + { + Log.Dispose(); + } + } + + static void Run(string[] args) { var arguments = new Arguments(args); diff --git a/OpenRA.Utility/Program.cs b/OpenRA.Utility/Program.cs index e3cc71745d..1b20596f7a 100644 --- a/OpenRA.Utility/Program.cs +++ b/OpenRA.Utility/Program.cs @@ -39,6 +39,18 @@ namespace OpenRA class Program { static void Main(string[] args) + { + try + { + Run(args); + } + finally + { + Log.Dispose(); + } + } + + static void Run(string[] args) { var engineDir = Environment.GetEnvironmentVariable("ENGINE_DIR"); if (!string.IsNullOrEmpty(engineDir))