From c14b8ed23a13dc4e13247b7c0141ec43997d1ff7 Mon Sep 17 00:00:00 2001 From: Westin Miller Date: Wed, 4 Oct 2017 18:57:47 -0700 Subject: [PATCH 1/3] Fixes #138 --- Torch/Managers/PatchManager/MSIL/MsilInstruction.cs | 1 - 1 file changed, 1 deletion(-) 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 { From d709bf68ddac19adf35774e4db45ef417b4a2857 Mon Sep 17 00:00:00 2001 From: Westin Miller Date: Wed, 4 Oct 2017 21:40:42 -0700 Subject: [PATCH 2/3] New system for resolving binaries --- Torch.Server/Initializer.cs | 6 +--- Torch.Server/Program.cs | 9 ++++-- Torch/Torch.csproj | 1 + Torch/Utils/TorchLauncher.cs | 63 ++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 Torch/Utils/TorchLauncher.cs 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/Torch.csproj b/Torch/Torch.csproj index 9d94c77..7bd9cb9 100644 --- a/Torch/Torch.csproj +++ b/Torch/Torch.csproj @@ -215,6 +215,7 @@ + diff --git a/Torch/Utils/TorchLauncher.cs b/Torch/Utils/TorchLauncher.cs new file mode 100644 index 0000000..39bcf77 --- /dev/null +++ b/Torch/Utils/TorchLauncher.cs @@ -0,0 +1,63 @@ +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 path = new StringBuilder(allPaths.First()); + foreach (string other in binaryPaths) + { + if (path.Length > other.Length) + path.Remove(other.Length, path.Length - other.Length); + for (var i = 0; i < path.Length; i++) + if (path[i] != other[i]) + { + path.Remove(i, path.Length - i); + break; + } + } + 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 = path.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); + } + } +} From 9e81b6316fd466083f5b5fa13f8fc59fb8199451 Mon Sep 17 00:00:00 2001 From: Westin Miller Date: Thu, 5 Oct 2017 12:56:16 -0700 Subject: [PATCH 3/3] Better debugging for freezing --- Torch.Server/TorchServer.cs | 72 +++++++++++++++++++++++++++++++----- Torch/Torch.csproj | 1 + Torch/Utils/StringUtils.cs | 64 ++++++++++++++++++++++++++++++++ Torch/Utils/TorchLauncher.cs | 16 +------- 4 files changed, 129 insertions(+), 24 deletions(-) create mode 100644 Torch/Utils/StringUtils.cs 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/Torch.csproj b/Torch/Torch.csproj index 7bd9cb9..d98b0c6 100644 --- a/Torch/Torch.csproj +++ b/Torch/Torch.csproj @@ -208,6 +208,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 index 39bcf77..70d4051 100644 --- a/Torch/Utils/TorchLauncher.cs +++ b/Torch/Utils/TorchLauncher.cs @@ -29,19 +29,7 @@ namespace Torch.Utils var allPaths = new HashSet { exePath }; foreach (string other in binaryPaths) allPaths.Add(other.ToLower().Replace('/', '\\')); - - var path = new StringBuilder(allPaths.First()); - foreach (string other in binaryPaths) - { - if (path.Length > other.Length) - path.Remove(other.Length, path.Length - other.Length); - for (var i = 0; i < path.Length; i++) - if (path[i] != other[i]) - { - path.Remove(i, path.Length - i); - break; - } - } + var pathPrefix = StringUtils.CommonPrefix(allPaths); AppDomain.CurrentDomain.AppendPrivatePath(String.Join(Path.PathSeparator.ToString(), allPaths)); AppDomain.CurrentDomain.SetData(TorchKey, true); AppDomain.CurrentDomain.ExecuteAssemblyByName(entryPoint, args); @@ -50,7 +38,7 @@ namespace Torch.Utils // exclude application base from probing var setup = new AppDomainSetup { - ApplicationBase = path.ToString(), + ApplicationBase = pathPrefix.ToString(), PrivateBinPathProbe = "", PrivateBinPath = string.Join(";", allPaths) };