diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 34e69521..65ee8303 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,6 +23,10 @@ jobs: - name: Build Winch run: dotnet build -c ${{ inputs.build_type }} + - name: Copy WinchConsole into Winch folder + run: | + cp -r WinchConsole/bin/* Winch/bin/ + - name: Upload Winch Artifact uses: actions/upload-artifact@v3 with: diff --git a/Winch.sln b/Winch.sln index 5696d732..4ceb150f 100644 --- a/Winch.sln +++ b/Winch.sln @@ -11,6 +11,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisasterButton", "Winch.Exa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleItems", "Winch.Examples\ExampleItems\ExampleItems.csproj", "{C112288E-E993-44F3-B7B0-FB4BD37F18EA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinchConsole", "WinchConsole\WinchConsole.csproj", "{D5CFABA9-FA58-474F-A394-E197BDF38FDD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinchCommon", "WinchCommon\WinchCommon.csproj", "{EF5F69E6-FD4D-4314-B187-A89D00BF9355}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +37,14 @@ Global {C112288E-E993-44F3-B7B0-FB4BD37F18EA}.Debug|Any CPU.Build.0 = Debug|Any CPU {C112288E-E993-44F3-B7B0-FB4BD37F18EA}.Release|Any CPU.ActiveCfg = Release|Any CPU {C112288E-E993-44F3-B7B0-FB4BD37F18EA}.Release|Any CPU.Build.0 = Release|Any CPU + {D5CFABA9-FA58-474F-A394-E197BDF38FDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5CFABA9-FA58-474F-A394-E197BDF38FDD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5CFABA9-FA58-474F-A394-E197BDF38FDD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5CFABA9-FA58-474F-A394-E197BDF38FDD}.Release|Any CPU.Build.0 = Release|Any CPU + {EF5F69E6-FD4D-4314-B187-A89D00BF9355}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF5F69E6-FD4D-4314-B187-A89D00BF9355}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF5F69E6-FD4D-4314-B187-A89D00BF9355}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF5F69E6-FD4D-4314-B187-A89D00BF9355}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Winch/Core/WinchCore.cs b/Winch/Core/WinchCore.cs index 6ba1d549..51687df2 100644 --- a/Winch/Core/WinchCore.cs +++ b/Winch/Core/WinchCore.cs @@ -4,21 +4,18 @@ using System.Collections.Generic; using System.IO; using System.Reflection; -using System.Text.RegularExpressions; -using UnityEngine.Profiling.Memory.Experimental; -using Winch.Config; using Winch.Logging; using Winch.Util; namespace Winch.Core { - public class WinchCore + public class WinchCore { public static Logger Log = new Logger(); public static Dictionary WinchModConfig = new(); - public static string WinchInstallLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + public static string WinchInstallLocation => Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); public static void Main() { diff --git a/Winch/Logging/LogSocket.cs b/Winch/Logging/LogSocket.cs new file mode 100644 index 00000000..33f68d53 --- /dev/null +++ b/Winch/Logging/LogSocket.cs @@ -0,0 +1,57 @@ +using Newtonsoft.Json; +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +namespace Winch.Logging +{ + public class LogSocket + { + private Socket _socket; + private readonly int _port; + private readonly Logger _logger; + + public LogSocket(Logger logger, int port) + { + _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _port = port; + _logger = logger; + } + + public void WriteToSocket(LogMessage logMessage) + { + if (_socket == null) + { + return; + } + + if (!_socket.Connected) + { + var endPoint = new IPEndPoint(IPAddress.Parse(Constants.IP), _port); + try + { + _socket?.Connect(endPoint); + } + catch (Exception ex) + { + _socket = null; + _logger.Error($"Could not connect to console at {Constants.IP}:{_port} - {ex}"); + } + } + + try + { + _socket?.Send(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(logMessage))); + } + catch (SocketException) { } + } + + public void Close() + { + Thread.Sleep(TimeSpan.FromSeconds(1)); + _socket?.Close(); + } + } +} diff --git a/Winch/Logging/Logger.cs b/Winch/Logging/Logger.cs index 9df58772..5701b92b 100644 --- a/Winch/Logging/Logger.cs +++ b/Winch/Logging/Logger.cs @@ -2,35 +2,67 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Net.Sockets; +using System.Net; using System.Text.RegularExpressions; using Winch.Config; +using Winch.Core; namespace Winch.Logging { - enum LogLevel - { - UNITY = 0, DEBUG = 1, INFO = 2, WARN = 3, ERROR = 4 - } - - public class Logger + public class Logger { private LogFile? _log; private LogFile? _latestLog; - private bool _writeLogs; + private bool _writeLogsToFile; + private bool _writeLogsToConsole; private LogLevel? _minLogLevel; - public Logger() - { - _writeLogs = WinchConfig.GetProperty("WriteLogsToFile", true); - if (!_writeLogs) - return; + private LogSocket? _logSocket; - _minLogLevel = (LogLevel)Enum.Parse(typeof(LogLevel), WinchConfig.GetProperty("LogLevel", "DEBUG")); - _log = new LogFile(); - _latestLog = new LogFile("latest.log"); + public string LogConsoleExe => Path.Combine(WinchCore.WinchInstallLocation, "WinchConsole.exe"); - CleanupLogs(); + public Logger() + { + _writeLogsToFile = WinchConfig.GetProperty("WriteLogsToFile", true); + if (_writeLogsToFile) + { + _minLogLevel = (LogLevel)Enum.Parse(typeof(LogLevel), WinchConfig.GetProperty("LogLevel", "DEBUG")); + _log = new LogFile(); + _latestLog = new LogFile("latest.log"); + CleanupLogs(); + } + + _writeLogsToConsole = WinchConfig.GetProperty("WriteLogsToConsole", true); + + if (_writeLogsToConsole) + { + // Find an avialable port for the logs + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + + // Console exe will get the port from the WinchConfig file + WinchConfig.SetProperty("LogPort", $"{port}"); + + Info($"Writing logs to port {port}"); + + try + { + Info($"Starting console at path {LogConsoleExe}"); + Process.Start(LogConsoleExe); + + _logSocket = new LogSocket(this, port); + } + catch (Exception e) + { + Error($"Could not start console : {e}"); + } + } + + Info($"Writing logs to file: {_writeLogsToFile}. Writing logs to console: {_writeLogsToConsole}."); } private static void CleanupLogs() @@ -63,14 +95,25 @@ private void Log(LogLevel level, string message) private void Log(LogLevel level, string message, string source) { - if (!_writeLogs) - return; - if (level < _minLogLevel) - return; - - string logMessage = $"[{GetLogTimestamp()}] [{source}] [{level}] : {message}"; - _log?.Write(logMessage); - _latestLog?.Write(logMessage); + if (level < _minLogLevel) + return; + + if (_writeLogsToConsole) + { + _logSocket?.WriteToSocket(new LogMessage() + { + Level = level, + Message = message, + Source = source + }); + } + + if (_writeLogsToFile) + { + string logMessage = $"[{GetLogTimestamp()}] [{source}] [{level}] : {message}"; + _log?.Write(logMessage); + _latestLog?.Write(logMessage); + } } private string GetLogTimestamp() diff --git a/Winch/Winch.csproj b/Winch/Winch.csproj index f6c5d406..c9749d35 100644 --- a/Winch/Winch.csproj +++ b/Winch/Winch.csproj @@ -16,21 +16,6 @@ - - - True - True - Resources.resx - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - PreserveNewest @@ -49,4 +34,9 @@ + + + + + diff --git a/Winch/Winch.nuspec b/Winch/Winch.nuspec index f507f1e8..a1634ad0 100644 --- a/Winch/Winch.nuspec +++ b/Winch/Winch.nuspec @@ -13,6 +13,7 @@ MIT - + + diff --git a/Winch/mod_meta.json b/Winch/mod_meta.json index 3366cd4e..427e4340 100644 --- a/Winch/mod_meta.json +++ b/Winch/mod_meta.json @@ -2,5 +2,5 @@ "Name": "Winch", "Author": "Hacktix", "ModGUID": "hacktix.winch", - "Version": "0.3.0" + "Version": "0.3.1" } diff --git a/Winch/Config/DefaultConfig.json b/WinchCommon/Config/DefaultConfig.json similarity index 84% rename from Winch/Config/DefaultConfig.json rename to WinchCommon/Config/DefaultConfig.json index f56e1971..264dc3ec 100644 --- a/Winch/Config/DefaultConfig.json +++ b/WinchCommon/Config/DefaultConfig.json @@ -1,5 +1,6 @@ { "WriteLogsToFile": true, + "WriteLogsToConsole": false, "LogLevel": "DEBUG", "LogsFolder": "Logs", "DetailedLogSources": false, diff --git a/Winch/Config/JSONConfig.cs b/WinchCommon/Config/JSONConfig.cs similarity index 100% rename from Winch/Config/JSONConfig.cs rename to WinchCommon/Config/JSONConfig.cs diff --git a/Winch/Config/ModConfig.cs b/WinchCommon/Config/ModConfig.cs similarity index 85% rename from Winch/Config/ModConfig.cs rename to WinchCommon/Config/ModConfig.cs index 3e28557f..5b05ef20 100644 --- a/Winch/Config/ModConfig.cs +++ b/WinchCommon/Config/ModConfig.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using Winch.Core; +using System.Reflection; namespace Winch.Config { - public class ModConfig : JSONConfig + public class ModConfig : JSONConfig { private static Dictionary DefaultConfigs = new Dictionary(); private static Dictionary Instances = new Dictionary(); @@ -18,7 +15,7 @@ private static string GetDefaultConfig(string modName) { if(!DefaultConfigs.ContainsKey(modName)) { - WinchCore.Log.Error($"No 'DefaultConfig' attribute found in mod_meta.json for {modName}!"); + //WinchCore.Log.Error($"No 'DefaultConfig' attribute found in mod_meta.json for {modName}!"); throw new KeyNotFoundException($"No 'DefaultConfig' attribute found in mod_meta.json for {modName}!"); } return DefaultConfigs[modName]; diff --git a/Winch/Config/WinchConfig.cs b/WinchCommon/Config/WinchConfig.cs similarity index 64% rename from Winch/Config/WinchConfig.cs rename to WinchCommon/Config/WinchConfig.cs index 912cdc9f..ef16ddc0 100644 --- a/Winch/Config/WinchConfig.cs +++ b/WinchCommon/Config/WinchConfig.cs @@ -7,7 +7,7 @@ public class WinchConfig : JSONConfig { private static readonly string WinchConfigPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "WinchConfig.json"); - private WinchConfig() : base(WinchConfigPath, Properties.Resources.DefaultConfig) { } + private WinchConfig() : base(WinchConfigPath, WinchCommon.Properties.Resources.DefaultConfig) { } private static WinchConfig? _instance; public static WinchConfig Instance @@ -22,7 +22,19 @@ public static WinchConfig Instance public static new T? GetProperty(string key, T defaultValue) { - return ((JSONConfig)Instance).GetProperty(key, defaultValue); + try + { + return ((JSONConfig)Instance).GetProperty(key, defaultValue); + } + catch + { + return defaultValue; + } } + + public static new void SetProperty(string key, T value) + { + ((JSONConfig)Instance).SetProperty(key, value); + } } } diff --git a/WinchCommon/Constants.cs b/WinchCommon/Constants.cs new file mode 100644 index 00000000..61f7263d --- /dev/null +++ b/WinchCommon/Constants.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Winch; + +public static class Constants +{ + public const string IP = "127.0.0.1"; +} diff --git a/WinchCommon/LogLevel.cs b/WinchCommon/LogLevel.cs new file mode 100644 index 00000000..ac20e5f8 --- /dev/null +++ b/WinchCommon/LogLevel.cs @@ -0,0 +1,6 @@ +namespace Winch; + +public enum LogLevel +{ + UNITY = 0, DEBUG = 1, INFO = 2, WARN = 3, ERROR = 4 +} diff --git a/WinchCommon/Logging/LogMessage.cs b/WinchCommon/Logging/LogMessage.cs new file mode 100644 index 00000000..42098df3 --- /dev/null +++ b/WinchCommon/Logging/LogMessage.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using WinchCommon; + +namespace Winch.Logging +{ + public class LogMessage + { + [JsonProperty("source")] + public string Source { get; set; } + + [JsonProperty("level")] + public LogLevel Level { get; set; } + + [JsonProperty("message")] + public string Message { get; set; } + } +} diff --git a/Winch/Properties/Resources.Designer.cs b/WinchCommon/Properties/Resources.Designer.cs similarity index 91% rename from Winch/Properties/Resources.Designer.cs rename to WinchCommon/Properties/Resources.Designer.cs index 0349a657..09afcb00 100644 --- a/Winch/Properties/Resources.Designer.cs +++ b/WinchCommon/Properties/Resources.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace Winch.Properties { +namespace WinchCommon.Properties { using System; @@ -39,7 +39,7 @@ internal Resources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Winch.Properties.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WinchCommon.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; @@ -63,10 +63,12 @@ internal Resources() { /// /// Looks up a localized string similar to { /// "WriteLogsToFile": true, + /// "WriteLogsToConsole": false, /// "LogLevel": "DEBUG", /// "LogsFolder": "Logs", /// "DetailedLogSources": false, - /// "EnableDeveloperConsole": true + /// "EnableDeveloperConsole": true, + /// "MaxLogFiles": 10 ///}. /// internal static string DefaultConfig { diff --git a/Winch/Properties/Resources.resx b/WinchCommon/Properties/Resources.resx similarity index 100% rename from Winch/Properties/Resources.resx rename to WinchCommon/Properties/Resources.resx diff --git a/WinchCommon/WinchCommon.csproj b/WinchCommon/WinchCommon.csproj new file mode 100644 index 00000000..0feb137a --- /dev/null +++ b/WinchCommon/WinchCommon.csproj @@ -0,0 +1,33 @@ + + + + net48 + enable + enable + 11 + + + + + + + + + + + + + Resources.resx + True + True + + + + + + Resources.Designer.cs + ResXFileCodeGenerator + + + + diff --git a/WinchConsole/LogSocketListener.cs b/WinchConsole/LogSocketListener.cs new file mode 100644 index 00000000..7945907a --- /dev/null +++ b/WinchConsole/LogSocketListener.cs @@ -0,0 +1,173 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using Winch.Config; +using Winch.Logging; + +namespace Winch +{ + public class LogSocketListener + { + private const string Separator = "\n--------------------------------"; + private const int BufferSize = 262144; + private static int _port; + private static TcpListener _server; + private bool _hasReceivedFatalMessage; + + public LogSocketListener() + { + + } + + public void Run() + { + var portStr = WinchConfig.GetProperty("LogPort", string.Empty); + + if (string.IsNullOrEmpty(portStr) || !int.TryParse(portStr, out _port)) + { + return; + } + + WriteByType(LogLevel.INFO, $"Setting up socket listener {_port}"); + try + { + ListenToSocket(); + } + catch (SocketException ex) + { + WriteByType(LogLevel.ERROR, $"Error in socket listener: {ex.SocketErrorCode} - {ex}"); + } + catch (Exception ex) + { + WriteByType(LogLevel.ERROR, $"Error while listening: {ex}"); + } + finally + { + _server?.Stop(); + } + } + + private void ListenToSocket() + { + var localAddress = IPAddress.Parse(Constants.IP); + + _server = new TcpListener(localAddress, _port); + _server.Start(); + WriteByType(LogLevel.INFO, $"Connected to local {localAddress}:{_port}"); + + var bytes = new byte[BufferSize]; + + while (true) + { + var client = _server.AcceptTcpClient(); + + WriteByType(LogLevel.INFO, "Console connected to socket!"); + + var stream = client.GetStream(); + + int i; + + while ((i = stream.Read(bytes, 0, bytes.Length)) != 0) + { + ProcessMessage(bytes, i); + } + + WriteByType(LogLevel.INFO, "Closing client!"); + client.Close(); + } + } + + private void ProcessMessage(byte[] bytes, int count) + { + var message = Encoding.UTF8.GetString(bytes, 0, count); + + var jsons = SplitMessage(message); + + foreach (var json in jsons) + { + if (string.IsNullOrWhiteSpace(json)) + { + return; + } + + ProcessJson(json); + } + } + + /// + /// Message can come in containing multiple JSON objects, we have to split them up into separate strings to parse + /// + /// + /// + private string[] SplitMessage(string message) + { + var jsons = new List(); + var jsonStart = 0; + var bracketCount = 0; + for (int i = 0; i < message.Length; i++) + { + if (message[i] == '{') + { + bracketCount++; + + } + else if (message[i] == '}') + { + bracketCount--; + if (bracketCount == 0) + { + var json = message.Substring(jsonStart, i + 1 - jsonStart); + jsons.Add(json); + jsonStart = i + 1; + } + } + } + + return jsons.Any() ? jsons.ToArray() : new string[] { message }; + } + + private void ProcessJson(string json) + { + LogMessage data; + try + { + data = JsonConvert.DeserializeObject(json) ?? throw new NullReferenceException(); + } + catch (Exception ex) + { + WriteByType(LogLevel.WARN, $"Failed to process following message:{Separator}\n{json}{Separator}"); + WriteByType(LogLevel.WARN, $"Reason: {ex}"); + return; + } + + var nameTypePrefix = $"[{data.Source}] : "; + + var messageData = data.Message; + messageData = messageData.Replace("\n", $"\n{new string(' ', nameTypePrefix.Length)}"); + + WriteByType(data.Level, $"{nameTypePrefix}{messageData}"); + } + + public static void WriteByType(LogLevel type, string line) + { + if (string.IsNullOrEmpty(line)) + { + return; + } + + Console.ForegroundColor = type switch + { + LogLevel.ERROR => ConsoleColor.Red, + LogLevel.WARN => ConsoleColor.Yellow, + LogLevel.DEBUG => ConsoleColor.DarkGray, + _ => ConsoleColor.White + }; + + Console.WriteLine(line); + } + } +} diff --git a/WinchConsole/Program.cs b/WinchConsole/Program.cs new file mode 100644 index 00000000..0c0c5f13 --- /dev/null +++ b/WinchConsole/Program.cs @@ -0,0 +1,32 @@ +using System; +using System.Diagnostics; +using Winch; + +/* + * Winch console logs were adapted from the log console for the Outer Wilds Mods Loader (OWML) under MIT license + * https://github.com/ow-mods/owml + */ + +namespace WinchConsole +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine("Loading Winch console!"); + + // Only allow one console to be open at a time + var currentProcess = Process.GetCurrentProcess(); + var duplicates = Process.GetProcessesByName(currentProcess.ProcessName); + + if (duplicates.Length > 1) + { + currentProcess.Kill(); + } + else + { + new LogSocketListener().Run(); + } + } + } +} \ No newline at end of file diff --git a/WinchConsole/WinchConsole.csproj b/WinchConsole/WinchConsole.csproj new file mode 100644 index 00000000..18ee2928 --- /dev/null +++ b/WinchConsole/WinchConsole.csproj @@ -0,0 +1,26 @@ + + + + Exe + net48 + 11 + false + false + + + + + + + + + False + False + + + + + + + +