diff --git a/Torch.Server/Initializer.cs b/Torch.Server/Initializer.cs index e4fe99e..7da5757 100644 --- a/Torch.Server/Initializer.cs +++ b/Torch.Server/Initializer.cs @@ -26,8 +26,7 @@ namespace Torch.Server login anonymous app_update 298740 quit"; - - private TorchAssemblyResolver _resolver; + private TorchConfig _config; private TorchServer _server; private string _basePath; @@ -50,7 +49,6 @@ quit"; if (!args.Contains("-noupdate")) RunSteamCmd(); - _resolver = new TorchAssemblyResolver(Path.Combine(_basePath, "DedicatedServer64")); _config = InitConfig(); if (!_config.Parse(args)) return false; @@ -94,8 +92,6 @@ quit"; } else _server.Start(); - - _resolver?.Dispose(); } private TorchConfig InitConfig() diff --git a/Torch.Server/Program.cs b/Torch.Server/Program.cs index efcc40a..ec745b2 100644 --- a/Torch.Server/Program.cs +++ b/Torch.Server/Program.cs @@ -44,13 +44,16 @@ namespace Torch.Server var binDir = Path.Combine(workingDir, "DedicatedServer64"); Directory.SetCurrentDirectory(workingDir); + if (!TorchLauncher.IsTorchWrapped()) + { + TorchLauncher.Launch(Assembly.GetEntryAssembly().FullName,args, binDir); + return; + } + if (!Environment.UserInteractive) { using (var service = new TorchService()) - using (new TorchAssemblyResolver(binDir)) - { ServiceBase.Run(service); - } return; } diff --git a/Torch.Server/TorchServer.cs b/Torch.Server/TorchServer.cs index afd6bfe..8937c3a 100644 --- a/Torch.Server/TorchServer.cs +++ b/Torch.Server/TorchServer.cs @@ -3,12 +3,14 @@ using Sandbox.Engine.Utils; using Sandbox.Game; using Sandbox.Game.World; using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Security.Principal; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Xml.Serialization.GeneratedAssembly; @@ -194,16 +196,62 @@ namespace Torch.Server ((TorchServer)state).Invoke(() => mre.Set()); if (!mre.WaitOne(TimeSpan.FromSeconds(Instance.Config.TickTimeout))) { - var mainThread = MySandboxGame.Static.UpdateThread; - if (mainThread.IsAlive) - mainThread.Suspend(); - var stackTrace = new StackTrace(mainThread, true); - throw new TimeoutException($"Server watchdog detected that the server was frozen for at least {((TorchServer)state).Config.TickTimeout} seconds.\n{stackTrace}"); + Log.Error(DumpFrozenThread(MySandboxGame.Static.UpdateThread)); + throw new TimeoutException($"Server watchdog detected that the server was frozen for at least {((TorchServer)state).Config.TickTimeout} seconds."); } Log.Debug("Server watchdog responded"); } + private static string DumpFrozenThread(Thread thread, int traces = 3, int pause = 5000) + { + var stacks = new List(traces); + var totalSize = 0; + for (var i = 0; i < traces; i++) + { + string dump = DumpStack(thread).ToString(); + totalSize += dump.Length; + stacks.Add(dump); + Thread.Sleep(pause); + } + string commonPrefix = StringUtils.CommonSuffix(stacks); + // Advance prefix to include the line terminator. + commonPrefix = commonPrefix.Substring(commonPrefix.IndexOf('\n') + 1); + + var result = new StringBuilder(totalSize - (stacks.Count - 1) * commonPrefix.Length); + result.AppendLine($"Frozen thread dump {thread.Name}"); + result.AppendLine("Common prefix:").AppendLine(commonPrefix); + for (var i = 0; i < stacks.Count; i++) + if (stacks[i].Length > commonPrefix.Length) + { + result.AppendLine($"Suffix {i}"); + result.AppendLine(stacks[i].Substring(0, stacks[i].Length - commonPrefix.Length)); + } + return result.ToString(); + } + + private static StackTrace DumpStack(Thread thread) + { + try + { + thread.Suspend(); + } + catch + { + // ignored + } + var stack = new StackTrace(thread, true); + try + { + thread.Resume(); + } + catch + { + // ignored + } + return stack; + } + /// public override void Stop() { @@ -253,28 +301,32 @@ namespace Torch.Server /// Caller of the save operation private void SaveCompleted(SaveGameStatus statusCode, long callerId = 0) { + string response = null; switch (statusCode) { case SaveGameStatus.Success: Log.Info("Save completed."); - // TODO -// Multiplayer.SendMessage("Saved game.", playerId: callerId); + response = "Saved game."; break; case SaveGameStatus.SaveInProgress: Log.Error("Save failed, a save is already in progress."); -// Multiplayer.SendMessage("Save failed, a save is already in progress.", playerId: callerId, font: MyFontEnum.Red); + response = "Save failed, a save is already in progress."; break; case SaveGameStatus.GameNotReady: Log.Error("Save failed, game was not ready."); -// Multiplayer.SendMessage("Save failed, game was not ready.", playerId: callerId, font: MyFontEnum.Red); + response = "Save failed, game was not ready."; break; case SaveGameStatus.TimedOut: Log.Error("Save failed, save timed out."); -// Multiplayer.SendMessage("Save failed, save timed out.", playerId: callerId, font: MyFontEnum.Red); + response = "Save failed, save timed out."; break; default: break; } + if (MySession.Static.Players.TryGetPlayerId(callerId, out MyPlayer.PlayerId result)) + { + Managers.GetManager()?.SendMessageAsOther("Server", response, statusCode == SaveGameStatus.Success ? MyFontEnum.Green : MyFontEnum.Red, result.SteamId); + } } } } diff --git a/Torch/Managers/PatchManager/MSIL/MsilInstruction.cs b/Torch/Managers/PatchManager/MSIL/MsilInstruction.cs index d16f8e1..f39072d 100644 --- a/Torch/Managers/PatchManager/MSIL/MsilInstruction.cs +++ b/Torch/Managers/PatchManager/MSIL/MsilInstruction.cs @@ -7,7 +7,6 @@ using System.Reflection.Emit; using System.Text; using Torch.Managers.PatchManager.Transpile; using Torch.Utils; -using Label = System.Windows.Controls.Label; namespace Torch.Managers.PatchManager.MSIL { diff --git a/Torch/Torch.csproj b/Torch/Torch.csproj index 0f59e6f..481927b 100644 --- a/Torch/Torch.csproj +++ b/Torch/Torch.csproj @@ -215,6 +215,7 @@ + @@ -222,6 +223,7 @@ + diff --git a/Torch/Utils/StringUtils.cs b/Torch/Utils/StringUtils.cs new file mode 100644 index 0000000..aade17e --- /dev/null +++ b/Torch/Utils/StringUtils.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Torch.Utils +{ + /// + /// Utility methods for strings + /// + public static class StringUtils + { + /// + /// Determines a common prefix for the given set of strings + /// + /// Set of strings + /// Common prefix + public static string CommonPrefix(IEnumerable set) + { + StringBuilder builder = null; + foreach (string other in set) + { + if (builder == null) + builder = new StringBuilder(other); + if (builder.Length > other.Length) + builder.Remove(other.Length, builder.Length - other.Length); + for (var i = 0; i < builder.Length; i++) + if (builder[i] != other[i]) + { + builder.Remove(i, builder.Length - i); + break; + } + } + return builder?.ToString() ?? ""; + } + + /// + /// Determines a common suffix for the given set of strings + /// + /// Set of strings + /// Common suffix + public static string CommonSuffix(IEnumerable set) + { + StringBuilder builder = null; + foreach (string other in set) + { + if (builder == null) + builder = new StringBuilder(other); + if (builder.Length > other.Length) + builder.Remove(0, builder.Length - other.Length); + for (var i = 0; i < builder.Length; i++) + { + if (builder[builder.Length - 1 - i] != other[other.Length - 1 - i]) + { + builder.Remove(0, builder.Length - i); + break; + } + } + } + return builder?.ToString() ?? ""; + } + } +} diff --git a/Torch/Utils/TorchLauncher.cs b/Torch/Utils/TorchLauncher.cs new file mode 100644 index 0000000..70d4051 --- /dev/null +++ b/Torch/Utils/TorchLauncher.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Torch.API; + +namespace Torch.Utils +{ + public class TorchLauncher + { + private const string TorchKey = "TorchWrapper"; + + public static bool IsTorchWrapped() + { + return AppDomain.CurrentDomain.GetData(TorchKey) != null; + } + + public static void Launch(string entryPoint, string[] args, params string[] binaryPaths) + { + if (IsTorchWrapped()) + throw new Exception("Can't wrap torch twice"); + string exePath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)?.ToLower().Replace('/', '\\'); + if (exePath == null) + throw new ArgumentException("Unable to determine executing assembly's path"); + var allPaths = new HashSet { exePath }; + foreach (string other in binaryPaths) + allPaths.Add(other.ToLower().Replace('/', '\\')); + var pathPrefix = StringUtils.CommonPrefix(allPaths); + AppDomain.CurrentDomain.AppendPrivatePath(String.Join(Path.PathSeparator.ToString(), allPaths)); + AppDomain.CurrentDomain.SetData(TorchKey, true); + AppDomain.CurrentDomain.ExecuteAssemblyByName(entryPoint, args); + return; + // this would be way better but HAVOK IS UNMANAGED :clang: + // exclude application base from probing + var setup = new AppDomainSetup + { + ApplicationBase = pathPrefix.ToString(), + PrivateBinPathProbe = "", + PrivateBinPath = string.Join(";", allPaths) + }; + AppDomain domain = AppDomain.CreateDomain($"TorchDomain-{Assembly.GetEntryAssembly().GetName().Name}-{new Random().Next():X}", null, setup); + domain.SetData(TorchKey, true); + domain.ExecuteAssemblyByName(entryPoint, args); + AppDomain.Unload(domain); + } + } +}