diff --git a/SecretAdmin/API/Events/EventArgs/ReceivedActionEventArgs.cs b/SecretAdmin/API/Events/EventArgs/ReceivedActionEventArgs.cs new file mode 100644 index 0000000..f647f18 --- /dev/null +++ b/SecretAdmin/API/Events/EventArgs/ReceivedActionEventArgs.cs @@ -0,0 +1,16 @@ +using SecretAdmin.Features.Server.Enums; + +namespace SecretAdmin.API.Events.EventArgs +{ + public class ReceivedActionEventArgs : System.EventArgs + { + public ReceivedActionEventArgs(byte actionCode) + { + OutputCode = (OutputCodes)actionCode; + IsEnabled = true; + } + + public OutputCodes OutputCode { get; set; } + public bool IsEnabled { get; set; } + } +} \ No newline at end of file diff --git a/SecretAdmin/API/Events/EventArgs/ReceivedMessageEventArgs.cs b/SecretAdmin/API/Events/EventArgs/ReceivedMessageEventArgs.cs new file mode 100644 index 0000000..4a36f69 --- /dev/null +++ b/SecretAdmin/API/Events/EventArgs/ReceivedMessageEventArgs.cs @@ -0,0 +1,16 @@ +namespace SecretAdmin.API.Events.EventArgs +{ + public class ReceivedMessageEventArgs : System.EventArgs + { + public ReceivedMessageEventArgs(string message, byte code) + { + Message = message; + Code = code; + IsAllowed = !string.IsNullOrWhiteSpace(message); + } + + public string Message { get; set; } + public byte Code { get; set; } + public bool IsAllowed { get; set; } + } +} \ No newline at end of file diff --git a/SecretAdmin/API/Events/Handlers/Server.cs b/SecretAdmin/API/Events/Handlers/Server.cs new file mode 100644 index 0000000..71fd956 --- /dev/null +++ b/SecretAdmin/API/Events/Handlers/Server.cs @@ -0,0 +1,19 @@ +using SecretAdmin.API.Events.EventArgs; + +namespace SecretAdmin.API.Events.Handlers +{ + public static class Server + { + public static event Utils.CustomEventHandler ReceivedMessage; + public static event Utils.CustomEventHandler ReceivedAction; + public static event Utils.CustomEventHandler Restarted; + public static event Utils.CustomEventHandler RestartedRound; + public static event Utils.CustomEventHandler RestartingRound; + + public static void OnReceivedMessage(ReceivedMessageEventArgs ev) => ReceivedMessage?.Invoke(ev); + public static void OnReceivedAction(ReceivedActionEventArgs ev) => ReceivedAction?.Invoke(ev); + public static void OnRestarted() => Restarted?.Invoke(); + public static void OnRestartedRound() => RestartedRound?.Invoke(); + public static void OnRestartingRound() => RestartingRound?.Invoke(); + } +} \ No newline at end of file diff --git a/SecretAdmin/API/Events/Utils.cs b/SecretAdmin/API/Events/Utils.cs new file mode 100644 index 0000000..b72ee76 --- /dev/null +++ b/SecretAdmin/API/Events/Utils.cs @@ -0,0 +1,8 @@ +namespace SecretAdmin.API.Events +{ + public static class Utils + { + public delegate void CustomEventHandler(T ev) where T : System.EventArgs; + public delegate void CustomEventHandler(); + } +} \ No newline at end of file diff --git a/SecretAdmin/API/Features/IModule.cs b/SecretAdmin/API/Features/IModule.cs new file mode 100644 index 0000000..a7ed982 --- /dev/null +++ b/SecretAdmin/API/Features/IModule.cs @@ -0,0 +1,15 @@ +using System; + +namespace SecretAdmin.API.Features +{ + public interface IModule + { + string Name { get; set; } + string Author { get; set; } + Version Version { get; set; } + + void OnEnabled(); + void OnDisabled(); + void OnRegisteringCommands(); + } +} \ No newline at end of file diff --git a/SecretAdmin/API/Features/Module.cs b/SecretAdmin/API/Features/Module.cs new file mode 100644 index 0000000..8042f5e --- /dev/null +++ b/SecretAdmin/API/Features/Module.cs @@ -0,0 +1,37 @@ +using System; +using System.Reflection; +using SecretAdmin.Features.Console; + +namespace SecretAdmin.API.Features +{ + public abstract class Module : IModule + { + protected Module() + { + Assembly = Assembly.GetCallingAssembly(); + Name ??= Assembly.GetName().Name; + Author ??= "Unknown"; + Version ??= Assembly.GetName().Version; + } + + private Assembly Assembly { get; } + public virtual string Name { get; set; } + public virtual string Author { get; set; } + public virtual Version Version { get; set; } + + public virtual void OnEnabled() + { + Log.Raw($"The module {Name} [{Version}] by {Author} was enabled.", ConsoleColor.DarkMagenta); + } + + public virtual void OnDisabled() + { + Log.Raw($"The module {Name} [{Version}] by {Author} was disabled.", ConsoleColor.DarkMagenta); + } + + public virtual void OnRegisteringCommands() + { + Program.CommandHandler.RegisterCommands(Assembly); + } + } +} \ No newline at end of file diff --git a/SecretAdmin/API/ModuleManager.cs b/SecretAdmin/API/ModuleManager.cs new file mode 100644 index 0000000..704145e --- /dev/null +++ b/SecretAdmin/API/ModuleManager.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using SecretAdmin.API.Features; +using SecretAdmin.Features.Console; +using SecretAdmin.Features.Program; +using Module = SecretAdmin.API.Features.Module; + +namespace SecretAdmin.API +{ + public static class ModuleManager + { + public static List Modules = new (); + + public static void LoadAll() + { + Log.WriteLine(); + Log.Raw("Loading modules dependencies!", ConsoleColor.DarkCyan); + + var startTime = DateTime.Now; + + foreach (var file in Directory.GetFiles(Paths.ModulesDependenciesFolder, "*.dll")) + { + try + { + var assembly = Assembly.UnsafeLoadFrom(file); + Log.Raw($"Dependency {assembly.GetName().Name} ({assembly.GetName().Version}) has been loaded!"); + } + catch (Exception e) + { + Log.Raw($"Couldn't load the dependency in the path {file}\n{e}", ConsoleColor.Red); + throw; + } + } + + Log.Raw($"Dependencies loaded in {(DateTime.Now - startTime).TotalMilliseconds}ms", ConsoleColor.Cyan); + Log.Raw("Loading modules!", ConsoleColor.DarkCyan); + + startTime = DateTime.Now; + + foreach (var file in Directory.GetFiles(Paths.ModulesFolder, "*.dll")) + { + var assembly = Assembly.Load(File.ReadAllBytes(file)); + + try + { + foreach (var type in assembly.GetTypes()) + { + if(type.IsAbstract || type.IsInterface || type.BaseType != typeof(Module)) + continue; + + var constructor = type.GetConstructor(Type.EmptyTypes); + if (constructor == null) + continue; + + var module = constructor.Invoke(null) as IModule; + module?.OnEnabled(); + module?.OnRegisteringCommands(); + + Modules.Add(module); + } + } + catch (Exception e) + { + Log.Raw(e, ConsoleColor.Red); + } + } + + Log.Raw($"Modules loaded in {(DateTime.Now - startTime).TotalMilliseconds}ms", ConsoleColor.Cyan); + } + } +} \ No newline at end of file diff --git a/SecretAdmin/Features/Console/Log.cs b/SecretAdmin/Features/Console/Log.cs index e14f6ca..e26fdfe 100644 --- a/SecretAdmin/Features/Console/Log.cs +++ b/SecretAdmin/Features/Console/Log.cs @@ -1,11 +1,13 @@ using System; -using System.Drawing; using System.Text.RegularExpressions; +using SecretAdmin.API.Events.EventArgs; using SConsole = System.Console; +using static SecretAdmin.Program; +using SEvents = SecretAdmin.API.Events.Handlers.Server; namespace SecretAdmin.Features.Console { - public class Log + public static class Log { private static readonly Regex FrameworksRegex = new (@"\[(DEBUG|INFO|WARN|ERROR)\] (\[.*?\]) (.*)", RegexOptions.Compiled | RegexOptions.Singleline); @@ -13,6 +15,7 @@ public class Log public static void Intro() { + SConsole.Clear(); WriteLine(@" .--. .-. .--. .-. _ : .--' .' `. : .; : : : :_; `. `. .--. .--. .--. .--.`. .' : : .-' :,-.,-.,-..-.,-.,-. @@ -22,7 +25,12 @@ public static void Intro() Write($"Secret Admin - Version v{SecretAdmin.Program.Version}"); WriteLine(" by Jesus-QC", ConsoleColor.Blue); WriteLine("Released under MIT License Copyright © Jesus-QC 2021", ConsoleColor.Red); + + if (!ConfigManager.SecretAdminConfig.ManualStart) + return; + WriteLine("Press any key to continue.", ConsoleColor.Green); + SConsole.ReadKey(); } public static void Input(string message, string title = "SERVER") @@ -32,7 +40,7 @@ public static void Input(string message, string title = "SERVER") Raw(message, ConsoleColor.Magenta, false); } - public static void Alert(string message, bool showTimeStamp = true) + public static void Alert(object message, bool showTimeStamp = true) { if (showTimeStamp) Write($"[{DateTime.Now:T}] ", ConsoleColor.DarkRed); @@ -44,7 +52,7 @@ public static void Alert(string message, bool showTimeStamp = true) // Alerts - public static void Raw(string message, ConsoleColor color = ConsoleColor.White, bool showTimeStamp = true) => WriteLine(showTimeStamp ? $"[{DateTime.Now:T}] {message}" : message, color); + public static void Raw(object message, ConsoleColor color = ConsoleColor.White, bool showTimeStamp = true) => WriteLine(showTimeStamp ? $"[{DateTime.Now:T}] {message}" : message, color); private static void Info(string title, string message) { @@ -78,21 +86,29 @@ private static void Warn(string title, string message) WriteLine(message, ConsoleColor.Yellow); } - private static void WriteLine(string message, ConsoleColor color = ConsoleColor.White) + public static void WriteLine(object message = null, ConsoleColor color = ConsoleColor.White) { SConsole.ForegroundColor = color; SConsole.WriteLine(message); + ProgramLogger?.AppendLog(message, true); } - private static void Write(string message, ConsoleColor color = ConsoleColor.White) + public static void Write(object message = null, ConsoleColor color = ConsoleColor.White) { SConsole.ForegroundColor = color; SConsole.Write(message); + ProgramLogger?.AppendLog(message); } public static void HandleMessage(string message, byte code) { - if(message == null) + var ev = new ReceivedMessageEventArgs(message, code); + SEvents.OnReceivedMessage(ev); + + message = ev.Message; + code = ev.Code; + + if(!ev.IsAllowed|| string.IsNullOrWhiteSpace(message)) return; var match = FrameworksRegex.Match(message); @@ -117,8 +133,6 @@ public static void HandleMessage(string message, byte code) break; } } - - } else { diff --git a/SecretAdmin/Features/Console/Logger.cs b/SecretAdmin/Features/Console/Logger.cs new file mode 100644 index 0000000..832b80b --- /dev/null +++ b/SecretAdmin/Features/Console/Logger.cs @@ -0,0 +1,24 @@ +using System.IO; + +namespace SecretAdmin.Features.Console +{ + public class Logger + { + private readonly string _path; + + public Logger(string path) + { + _path = path; + } + + public void AppendLog(object message, bool newLine = false) + { + using var stream = File.AppendText(_path); + + if (newLine) + stream.WriteLine(message); + else + stream.Write(message); + } + } +} \ No newline at end of file diff --git a/SecretAdmin/Features/Program/ArgumentsManager.cs b/SecretAdmin/Features/Program/ArgumentsManager.cs index c95e2c5..9d79883 100644 --- a/SecretAdmin/Features/Program/ArgumentsManager.cs +++ b/SecretAdmin/Features/Program/ArgumentsManager.cs @@ -1,15 +1,43 @@ namespace SecretAdmin.Features.Program { - public class ArgumentsManager + public static class ArgumentsManager { // TODO: this /* * Arguments: * --reconfigure -r - * --config -c - * --logs -l - * --game-logs -gl + * --config -c * --no-logs -nl */ + + public static Args GetArgs(string[] args) + { + var ret = new Args(); + + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "--reconfigure" or "-r": + ret.Reconfigure = true; + break; + case "--config" or "-c" when args.Length > i + 1: + ret.Config = args[i + 1]; + break; + case "--no-logs" or "-nl": + ret.Logs = false; + break; + } + } + + return ret; + } + + public class Args + { + public bool Reconfigure = false; + public string Config = "default.yml"; + public bool Logs = true; + } } } \ No newline at end of file diff --git a/SecretAdmin/Features/Program/AutoUpdater.cs b/SecretAdmin/Features/Program/AutoUpdater.cs new file mode 100644 index 0000000..7438b6f --- /dev/null +++ b/SecretAdmin/Features/Program/AutoUpdater.cs @@ -0,0 +1,10 @@ +namespace SecretAdmin.Features.Program +{ + public static class AutoUpdater + { + public static void CheckForUpdates() + { + // todo: this + } + } +} \ No newline at end of file diff --git a/SecretAdmin/Features/Program/Config/ConfigManager.cs b/SecretAdmin/Features/Program/Config/ConfigManager.cs new file mode 100644 index 0000000..5152587 --- /dev/null +++ b/SecretAdmin/Features/Program/Config/ConfigManager.cs @@ -0,0 +1,45 @@ +using System.IO; +using YamlDotNet.Serialization; + +namespace SecretAdmin.Features.Program.Config +{ + public class ConfigManager + { + public MainConfig SecretAdminConfig { get; private set; } = new (); + private readonly Serializer _serializer = new (); + private readonly Deserializer _deserializer = new (); + + public void LoadConfig() + { + if(!File.Exists(Paths.ProgramConfig)) + SaveConfig(new MainConfig()); + + SecretAdminConfig = _deserializer.Deserialize(File.ReadAllText(Paths.ProgramConfig)); + } + + public void SaveConfig(MainConfig config) + { + File.WriteAllText(Paths.ProgramConfig, _serializer.Serialize(config)); + LoadConfig(); + } + + public void SaveServerConfig(ServerConfig config) + { + File.WriteAllText(Path.Combine(Paths.ServerConfigsFolder, "default.yml"), _serializer.Serialize(config)); + File.WriteAllText(Path.Combine(Paths.ServerConfigsFolder, "7777.yml"), _serializer.Serialize(config)); + } + + public ServerConfig GetServerConfig(string name) + { + var def = Path.Combine(Paths.ServerConfigsFolder, "default.yml"); + + if(!File.Exists(def)) + SaveServerConfig(new ServerConfig()); + + if (name != null && File.Exists(Path.Combine(Paths.ServerConfigsFolder, name))) + return _deserializer.Deserialize(File.ReadAllText(Path.Combine(Paths.ServerConfigsFolder, name))); + + return _deserializer.Deserialize(File.ReadAllText(def)); + } + } +} \ No newline at end of file diff --git a/SecretAdmin/Features/Program/Config/MainConfig.cs b/SecretAdmin/Features/Program/Config/MainConfig.cs index ea16374..213a864 100644 --- a/SecretAdmin/Features/Program/Config/MainConfig.cs +++ b/SecretAdmin/Features/Program/Config/MainConfig.cs @@ -2,7 +2,12 @@ { public class MainConfig { - // TODO: this - public bool AutoUpdater; + public bool AutoUpdater { get; set; } = true; + public bool RestartOnCrash { get; set; } = true; + public int ArchiveLogsDays { get; set; } = 1; + public bool ManualStart { get; set; } = false; + public bool SafeShutdown { get; set; } = true; + public bool RestartWithLowMemory { get; set; } = true; + public int MaxDefaultMemory { get; set; } = 2048; } } \ No newline at end of file diff --git a/SecretAdmin/Features/Program/Config/ServerConfig.cs b/SecretAdmin/Features/Program/Config/ServerConfig.cs index db566fd..d2e6626 100644 --- a/SecretAdmin/Features/Program/Config/ServerConfig.cs +++ b/SecretAdmin/Features/Program/Config/ServerConfig.cs @@ -2,6 +2,7 @@ { public class ServerConfig { - // TODO: this + public uint Port { get; set; } = 7777; + public int RoundsToRestart { get; set; } = -1; } } \ No newline at end of file diff --git a/SecretAdmin/Features/Program/ExiledInstaller.cs b/SecretAdmin/Features/Program/ExiledInstaller.cs new file mode 100644 index 0000000..1a5539d --- /dev/null +++ b/SecretAdmin/Features/Program/ExiledInstaller.cs @@ -0,0 +1,50 @@ +using System.Diagnostics; +using System.Net; +using System.Runtime.InteropServices; +using SecretAdmin.Features.Console; + +namespace SecretAdmin.Features.Program +{ + public static class ExiledInstaller + { + public static void InstallExiled() + { + var platformSpecificString = "Linux"; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + platformSpecificString = "Win.exe"; + + Log.Alert("Downloading EXILED..."); + + using (var client = new WebClient()) + client.DownloadFile($"https://github.com/Exiled-Team/EXILED/releases/latest/download/Exiled.Installer-{platformSpecificString}", $"Exiled.Installer-{platformSpecificString}"); + + Log.Alert("Running installer..."); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Log.Alert("Marking installer as executable..."); + + using var p = new Process(); + + p.StartInfo.FileName = "/bin/bash"; + p.StartInfo.Arguments = "-c \" chmod +x ./Exiled.Installer-Linux\" "; + p.StartInfo.CreateNoWindow = true; + + p.Start(); + p.WaitForExit(); + } + + using (Process p = new Process()) + { + p.StartInfo.UseShellExecute = false; + p.StartInfo.RedirectStandardOutput = true; + p.StartInfo.FileName = $"Exiled.Installer-{platformSpecificString}"; + p.Start(); + p.WaitForExit(); + } + + Log.Alert("Done! Exiled was installed correctly."); + } + } +} \ No newline at end of file diff --git a/SecretAdmin/Features/Program/InputManager.cs b/SecretAdmin/Features/Program/InputManager.cs index b92f9a5..95c775d 100644 --- a/SecretAdmin/Features/Program/InputManager.cs +++ b/SecretAdmin/Features/Program/InputManager.cs @@ -1,9 +1,8 @@ -using System; -using SecretAdmin.Features.Console; +using SecretAdmin.Features.Console; namespace SecretAdmin.Features.Program { - public class InputManager + public static class InputManager { public static void Start() { diff --git a/SecretAdmin/Features/Program/Paths.cs b/SecretAdmin/Features/Program/Paths.cs index 5837deb..865b6e3 100644 --- a/SecretAdmin/Features/Program/Paths.cs +++ b/SecretAdmin/Features/Program/Paths.cs @@ -2,13 +2,16 @@ namespace SecretAdmin.Features.Program { - public class Paths + public static class Paths { public static string MainFolder { get; private set; } public static string LogsFolder { get; private set; } public static string ServerLogsFolder { get; private set; } public static string ProgramLogsFolder { get; private set; } + public static string ServerConfigsFolder { get; private set; } public static string ProgramConfig { get; private set; } + public static string ModulesFolder { get; private set; } + public static string ModulesDependenciesFolder { get; private set; } public static void Load() { @@ -16,7 +19,10 @@ public static void Load() LogsFolder = Path.Combine(MainFolder, "Logs"); ServerLogsFolder = Path.Combine(LogsFolder, "Server"); ProgramLogsFolder = Path.Combine(LogsFolder, "SecretAdmin"); + ServerConfigsFolder = Path.Combine(MainFolder, "Configs"); ProgramConfig = Path.Combine(MainFolder, "config.yml"); + ModulesFolder = Path.Combine(MainFolder, "Modules"); + ModulesDependenciesFolder = Path.Combine(ModulesFolder, "Dependencies"); CreateIfNotExists(); } @@ -26,6 +32,9 @@ public static void CreateIfNotExists() Directory.CreateDirectory(LogsFolder); Directory.CreateDirectory(ServerLogsFolder); Directory.CreateDirectory(ProgramLogsFolder); + Directory.CreateDirectory(ServerConfigsFolder); + Directory.CreateDirectory(ModulesFolder); + Directory.CreateDirectory(ModulesDependenciesFolder); } } } \ No newline at end of file diff --git a/SecretAdmin/Features/Program/ProgramIntroduction.cs b/SecretAdmin/Features/Program/ProgramIntroduction.cs index 49b0dc6..e78b0fc 100644 --- a/SecretAdmin/Features/Program/ProgramIntroduction.cs +++ b/SecretAdmin/Features/Program/ProgramIntroduction.cs @@ -1,23 +1,54 @@ using System; using System.IO; using SecretAdmin.Features.Console; +using SecretAdmin.Features.Program.Config; namespace SecretAdmin.Features.Program { - public class ProgramIntroduction + public static class ProgramIntroduction { - public static bool FirstTime => !Directory.Exists("SecretAdmin"); + public static bool FirstTime => !File.Exists(Paths.ProgramConfig); public static void ShowIntroduction() { - System.Console.WriteLine(); + Log.Intro(); + Log.WriteLine(); Log.Alert("Hi, welcome to SecretAdmin!"); Log.Alert("It seems like your first time using it, so we have to configure some things before!"); - System.Console.ForegroundColor = ConsoleColor.Green; - System.Console.WriteLine("Press any key to continue."); + Log.WriteLine("Press any key to continue.", ConsoleColor.Green); System.Console.ReadKey(); - // Options + // Program Options + + var cfg = new MainConfig + { + AutoUpdater = GetOption("Do you want to enable the auto updater?"), + ManualStart = GetOption("Do you want to manually have to enter a key to start the server?"), + SafeShutdown = GetOption("Do you want to safe shutdown the game processes?"), + ArchiveLogsDays = GetOption("In how many days the logs should be archived?", "1"), + RestartOnCrash = GetOption("Should the server automatically restart itself when it crashes?"), + RestartWithLowMemory = GetOption("Should the server restart itself when it has low memory?"), + MaxDefaultMemory = GetOption("Max memory the server can use, in MB.", "2048") + }; + + Paths.Load(); + SecretAdmin.Program.ConfigManager.SaveConfig(cfg); + + Log.WriteLine(); + Log.Raw("That were all the program configs! You can edit them always in /SecretAdmin/config.yml.", ConsoleColor.Cyan); + Log.Alert("Time to edit the default server configs."); + + // Server Options + + var srvConfig = new ServerConfig() + { + Port = (uint)GetOption("Which should be the default server port?", "7777"), + RoundsToRestart = GetOption("In how many rounds the server should restart itself. -1 disable, 0 every round", "-1") + }; + + SecretAdmin.Program.ConfigManager.SaveServerConfig(srvConfig); + + // Start the server Log.Alert("Ok, thats all! Time to enjoy the server :)"); System.Console.ForegroundColor = ConsoleColor.Green; @@ -25,5 +56,29 @@ public static void ShowIntroduction() System.Console.ReadKey(); Directory.CreateDirectory("SecretAdmin"); } + + private static bool GetOption(string msg) + { + START: + Log.Alert($"{msg} yes (y) / no (n)"); + var opt = System.Console.ReadLine()?.ToLower(); + if (!string.IsNullOrWhiteSpace(opt) && (opt[0] == 'y' || opt[0] == 'n')) + return opt[0] == 'y'; + Log.Alert("An error occurred parsing the input, please try again!"); + goto START; + } + + private static int GetOption(string msg, string def) + { + START: + Log.Alert($"{msg} introduce a number. (default = {def})"); + var opt = System.Console.ReadLine(); + if (string.IsNullOrWhiteSpace(opt)) + return int.Parse(def); + if (int.TryParse(opt, out var z)) + return z; + Log.Alert("An error occurred parsing the input, please try again!"); + goto START; + } } } \ No newline at end of file diff --git a/SecretAdmin/Features/Server/Commands/CommandHandler.cs b/SecretAdmin/Features/Server/Commands/CommandHandler.cs index e63b58f..63503c4 100644 --- a/SecretAdmin/Features/Server/Commands/CommandHandler.cs +++ b/SecretAdmin/Features/Server/Commands/CommandHandler.cs @@ -1,7 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using SecretAdmin.Features.Console; +using SecretAdmin.Features.Program; using SecretAdmin.Features.Server.Enums; namespace SecretAdmin.Features.Server.Commands @@ -13,7 +16,13 @@ public class CommandHandler [ConsoleCommand("Ram")] private void ShowRamUsage() { - Log.Alert($"RAM USAGE: (X)MB"); // TODO: calculate this + Log.Alert($"RAM USAGE: {SecretAdmin.Program.Server.MemoryManager.GetMemory()}MB"); // TODO: calculate this + } + + [ConsoleCommand("exiled")] + private void ExiledInstall() + { + ExiledInstaller.InstallExiled(); } [ConsoleCommand("Quit")] @@ -32,9 +41,8 @@ private void ExitCommand() public CommandHandler() { var ti = typeof(CommandHandler).GetTypeInfo(); - var methods = ti.DeclaredMethods; - foreach (var method in methods) + foreach (var method in ti.DeclaredMethods) { var attributes = method.GetCustomAttributes(); @@ -43,6 +51,36 @@ public CommandHandler() } } + public void RegisterCommands(Assembly assembly) + { + foreach (var type in assembly.GetTypes()) + { + foreach (var method in type.GetTypeInfo().DeclaredMethods) + { + var attributes = method.GetCustomAttributes(); + + if (attributes.FirstOrDefault() is not ConsoleCommandAttribute query) + continue; + + if (!method.IsStatic) + { + Log.Raw($"[Warn] The command {query.Name} couldn't be registered due to not being static.", ConsoleColor.DarkYellow); + continue; + } + + var cmd = query.Name.ToLower(); + + if (_commands.ContainsKey(cmd)) + { + Log.Raw($"[Error] The command \"{query.Name}\" already exists inside the module {_commands[cmd].Module.Assembly.GetName().Name}.", ConsoleColor.Red); + continue; + } + + _commands.Add(cmd, method); + } + } + } + public bool SendCommand(string name) { name = name.ToLower(); diff --git a/SecretAdmin/Features/Server/Enums/OutputCodes.cs b/SecretAdmin/Features/Server/Enums/OutputCodes.cs index ca8a8fe..0b5dbdb 100644 --- a/SecretAdmin/Features/Server/Enums/OutputCodes.cs +++ b/SecretAdmin/Features/Server/Enums/OutputCodes.cs @@ -10,7 +10,6 @@ public enum OutputCodes : byte ExitActionReset = 0x13, ExitActionShutdown = 0x14, ExitActionSilentShutdown = 0x15, - ExitActionRestart = 0x16, - RoundEnd = 0x17 + ExitActionRestart = 0x16 } } \ No newline at end of file diff --git a/SecretAdmin/Features/Server/MemoryManager.cs b/SecretAdmin/Features/Server/MemoryManager.cs new file mode 100644 index 0000000..8ae839d --- /dev/null +++ b/SecretAdmin/Features/Server/MemoryManager.cs @@ -0,0 +1,57 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using SecretAdmin.Features.Console; +using SecretAdmin.Features.Server.Enums; +using static SecretAdmin.Program; + +namespace SecretAdmin.Features.Server +{ + public class MemoryManager : IDisposable + { + private Process _process; + private bool _killed; + + public MemoryManager(Process process) => _process = process; + + public void Start() + { + _killed = false; + Task.Run(CheckUse); + } + + private async void CheckUse() + { + await Task.Delay(5000); + while (!_killed) + { + if(SecretAdmin.Program.Server.Status != ServerStatus.Online) + return; + + var mem = GetMemory(); + + if (mem > ConfigManager.SecretAdminConfig.MaxDefaultMemory) + { + Log.Raw($"LOW MEMORY. USING {mem}MB / {ConfigManager.SecretAdminConfig.MaxDefaultMemory}"); + await Task.Delay(2500); + + if (ConfigManager.SecretAdminConfig.RestartWithLowMemory) + { + SecretAdmin.Program.Server.ForceRestart(); + return; + } + } + + await Task.Delay(5000); + } + } + + public long GetMemory() + { + _process.Refresh(); + return _process.WorkingSet64 / 1048576; + } + + public void Dispose() => _killed = true; + } +} \ No newline at end of file diff --git a/SecretAdmin/Features/Server/ScpServer.cs b/SecretAdmin/Features/Server/ScpServer.cs index dcd5f23..4138715 100644 --- a/SecretAdmin/Features/Server/ScpServer.cs +++ b/SecretAdmin/Features/Server/ScpServer.cs @@ -1,58 +1,66 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using System.Threading; using SecretAdmin.Features.Console; +using SecretAdmin.Features.Program.Config; using SecretAdmin.Features.Server.Enums; -using static System.String; +using SEvents = SecretAdmin.API.Events.Handlers.Server; namespace SecretAdmin.Features.Server { public class ScpServer { + public MemoryManager MemoryManager { get; private set; } public SocketServer Socket { get; private set; } - public ServerStatus Status; + public ServerConfig Config { get; } - private Process _serverProcess; - private readonly uint _port; + public ServerStatus Status; + public DateTime StartedTime; //TODO: . + public int Rounds; //TODO: . - public DateTime StartedTime; - public int Rounds; + private Process _serverProcess; + private Logger _logger; + private Logger _outputLogger; - public ScpServer(uint port) => _port = port; + public ScpServer(ServerConfig config) => Config = config; public void Start() { - var fileName = "SCPSL.x86_64"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - fileName = "SCPSL.exe"; - - if (!File.Exists(fileName)) + if (!Utils.GetExecutable(out var fileName)) { - System.Console.WriteLine(); - Log.Alert("Executable not found, make sure this file is on the same folder as LocalAdmin."); - System.Console.ReadLine(); + System.Console.ReadKey(); Environment.Exit(-1); + return; } + Utils.ArchiveServerLogs(); + _logger = new Logger(Utils.GetLogsName(Config.Port)); + _outputLogger = new Logger(Utils.GetOutputLogsName(Config.Port)); + _serverProcess?.Dispose(); Socket = new SocketServer(this); - var gameArgs = new List { "-batchmode", "-nographics", "-silent-crashes", "-nodedicateddelete", $"-id{Process.GetCurrentProcess().Id}", $"-console{Socket.Port}", $"-port{_port}" }; - var startInfo = new ProcessStartInfo(fileName, Join(' ', gameArgs)) { CreateNoWindow = true, UseShellExecute = false }; - - System.Console.WriteLine(); - Log.Alert("Starting server on port 7777."); - System.Console.WriteLine(); + var gameArgs = new List { "-batchmode", "-nographics", "-silent-crashes", "-nodedicateddelete", $"-id{Process.GetCurrentProcess().Id}", $"-console{Socket.Port}", $"-port{Config.Port}" }; + var startInfo = new ProcessStartInfo(fileName, string.Join(' ', gameArgs)) { CreateNoWindow = true, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true }; + Log.WriteLine(); + Log.Alert($"Starting server on port {Config.Port}."); + Log.WriteLine(); + _serverProcess = Process.Start(startInfo); _serverProcess!.Exited += OnExited; _serverProcess.EnableRaisingEvents = true; - + _serverProcess.ErrorDataReceived += (_, args) => AddOutputLog(args.Data, "[STDERR]"); + _serverProcess.OutputDataReceived += (_, args) => AddOutputLog(args.Data, "[STDOUT]"); + _serverProcess.BeginErrorReadLine(); + _serverProcess.BeginOutputReadLine(); + Status = ServerStatus.Online; StartedTime = DateTime.Now; + Rounds = 0; + + MemoryManager = new MemoryManager(_serverProcess); + MemoryManager.Start(); } private void OnExited(object o, EventArgs e) @@ -61,11 +69,13 @@ private void OnExited(object o, EventArgs e) { case ServerStatus.Restarting: Restart(); - break; + return; + case ServerStatus.Exiting: Kill(); Environment.Exit(0); - break; + return; + case ServerStatus.Online: Log.Raw(@" █████████ █████ ███ @@ -75,9 +85,15 @@ private void OnExited(object o, EventArgs e) ░███ ░███ ░░░ ███████ ░░█████ ░███ ░███ ░███ ░░███ ███ ░███ ███░░███ ░░░░███ ░███ ░███ ░░░ ░░█████████ █████ ░░████████ ██████ ████ █████ ███ - ░░░░░░░░░ ░░░░░ ░░░░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░ ", ConsoleColor.DarkYellow, false); - Restart(); - break; + ░░░░░░░░░ ░░░░░ ░░░░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░ ", + ConsoleColor.DarkYellow, false); + + if (SecretAdmin.Program.ConfigManager.SecretAdminConfig.RestartOnCrash) + Restart(); + else + Log.Raw("Server crashed, press any key to close SecretAdmin."); System.Console.ReadKey(); + return; + default: throw new ArgumentOutOfRangeException(); } @@ -85,6 +101,8 @@ private void OnExited(object o, EventArgs e) public void Kill() { + MemoryManager.Dispose(); + MemoryManager = null; Socket?.Dispose(); Socket = null; _serverProcess?.Kill(); @@ -92,8 +110,31 @@ public void Kill() public void Restart() { + SEvents.OnRestarted(); Kill(); Start(); } + + public void ForceRestart() + { + Status = ServerStatus.Restarting; + Restart(); + } + + public void AddLog(string message, string title = null) + { + if(string.IsNullOrEmpty(message)) + return; + + _logger.AppendLog(title == null ? message : $"{title} {message}", true); + } + + public void AddOutputLog(string message, string title = null) + { + if(string.IsNullOrEmpty(message)) + return; + + _outputLogger.AppendLog(title == null ? message : $"{title} {message}", true); + } } } \ No newline at end of file diff --git a/SecretAdmin/Features/Server/SilentCrashHandler.cs b/SecretAdmin/Features/Server/SilentCrashHandler.cs index 5f4c4d5..c46c3c4 100644 --- a/SecretAdmin/Features/Server/SilentCrashHandler.cs +++ b/SecretAdmin/Features/Server/SilentCrashHandler.cs @@ -1,20 +1,42 @@ -namespace SecretAdmin.Features.Server +using System; +using System.Threading.Tasks; +using SecretAdmin.Features.Console; +using Main = SecretAdmin.Program; + +namespace SecretAdmin.Features.Server { - public class SilentCrashHandler + public class SilentCrashHandler : IDisposable { - private ScpServer _server; + private readonly SocketServer _server; + private bool _killed; + private int _pingCount; + + public SilentCrashHandler(SocketServer server) => _server = server; - // TODO: this - - public SilentCrashHandler(ScpServer server) + public void Start() { - _server = server; - + _killed = false; + Task.Run(SendPing); } - public void Start() + private async void SendPing() { - + await Task.Delay(15000); + while (!_killed) + { + _server.SendMessage("saping"); + _pingCount++; + await Task.Delay(5000); + if (_pingCount == 3) + { + Log.Alert("The server silently crashed, restarting...."); + Main.Server.ForceRestart(); + } + } } + + public void OnReceivePing() => _pingCount = 0; + + public void Dispose() => _killed = true; } } \ No newline at end of file diff --git a/SecretAdmin/Features/Server/SocketServer.cs b/SecretAdmin/Features/Server/SocketServer.cs index 7bff5b9..31ea783 100644 --- a/SecretAdmin/Features/Server/SocketServer.cs +++ b/SecretAdmin/Features/Server/SocketServer.cs @@ -1,13 +1,12 @@ using System; -using System.Diagnostics; -using System.Globalization; using System.Net; -using System.Net.Sockets; using System.Text; -using System.Threading; +using System.Net.Sockets; using System.Threading.Tasks; +using SecretAdmin.API.Events.EventArgs; using SecretAdmin.Features.Console; using SecretAdmin.Features.Server.Enums; +using SEvents = SecretAdmin.API.Events.Handlers.Server; namespace SecretAdmin.Features.Server { @@ -20,6 +19,7 @@ public class SocketServer : IDisposable private readonly TcpListener _listener; private TcpClient _client; private NetworkStream _stream; + private SilentCrashHandler _crashHandler; public SocketServer(ScpServer server) { @@ -35,6 +35,9 @@ public SocketServer(ScpServer server) _stream = _client.GetStream(); Task.Run(ListenRequests); }, _listener); + + _crashHandler = new SilentCrashHandler(this); + _crashHandler.Start(); } public async void ListenRequests() @@ -51,14 +54,14 @@ public async void ListenRequests() var messageBuffer = new byte[length]; var messageBytesRead = await _stream.ReadAsync(messageBuffer, 0, length); - + if (codeBytes <= 0 || lengthBytes != sizeof(int) || messageBytesRead <= 0) { if(_server.Status == ServerStatus.Online) Log.Alert("Socket disconnected."); break; } - + if (codeType >= 16) { HandleAction(codeType); @@ -69,12 +72,17 @@ public async void ListenRequests() return; var message = Encoding.GetString(messageBuffer, 0, length); - Log.HandleMessage(message, codeType); + if (HandleSecretAdminEvents(message)) + { + _server.AddLog(message, $"[{DateTime.Now:T}]"); + Log.HandleMessage(message, codeType); + } } } public void Dispose() { + _crashHandler?.Dispose(); _client?.Close(); _listener?.Stop(); } @@ -83,7 +91,7 @@ public void SendMessage(string message) { if (_stream == null) { - Log.Alert($"The server hasn't been initialized yet"); + Log.Alert("The server hasn't been initialized yet"); return; } @@ -104,49 +112,71 @@ public void SendMessage(string message) public void HandleAction(byte action) { - switch ((OutputCodes)action) + var ev = new ReceivedActionEventArgs(action); + SEvents.OnReceivedAction(ev); + + if(!ev.IsEnabled) + return; + + switch (ev.OutputCode) { - // This seems to show up at the waiting for players event case OutputCodes.RoundRestart: + SEvents.OnRestartedRound(); Log.Raw("Waiting for players.", ConsoleColor.DarkCyan); + _server.AddLog("Waiting for players."); break; case OutputCodes.IdleEnter: Log.Raw("Server entered idle mode.", ConsoleColor.DarkYellow); + _server.AddLog("Server entered idle mode."); break; case OutputCodes.IdleExit: Log.Raw("Server exited idle mode.", ConsoleColor.DarkYellow); + _server.AddLog("Server exited idle mode."); break; case OutputCodes.ExitActionReset: - Log.Alert("ExitActionReset."); // DEBUG: Dont remove _server.Status = ServerStatus.Online; break; case OutputCodes.ExitActionShutdown: - Log.Alert("ExitActionShutdown."); // DEBUG: Dont remove _server.Status = ServerStatus.Exiting; break; case OutputCodes.ExitActionSilentShutdown: - Log.Alert("ExitActionSilentShutdown."); // DEBUG: Dont remove _server.Status = ServerStatus.Exiting; break; case OutputCodes.ExitActionRestart: - Log.Alert("ExitActionRestart"); // DEBUG: Dont remove _server.Status = ServerStatus.Restarting; break; - case OutputCodes.RoundEnd: - Log.Alert("RoundEnd"); // DEBUG: Dont remove - break; - default: - Log.Alert($"Received unknown output code ({action}) ({(OutputCodes)action}), is SecretAdmin up to date?"); + Log.Alert($"Received unknown output code ({(OutputCodes)action}), is SecretAdmin up to date?"); break; } } + + private bool HandleSecretAdminEvents(string message) + { + if (message == "Command saping does not exist!") + { + _crashHandler.OnReceivePing(); + return false; + } + + if (message.StartsWith("Round finished!") || message.StartsWith("Round restart forced.")) + { + SEvents.OnRestartingRound(); + _server.Rounds++; + + if (_server.Config.RoundsToRestart >= _server.Rounds && _server.Status == ServerStatus.Online) + _server.ForceRestart(); + return true; + } + + return true; + } } } \ No newline at end of file diff --git a/SecretAdmin/Features/Server/Utils.cs b/SecretAdmin/Features/Server/Utils.cs new file mode 100644 index 0000000..d82f0ac --- /dev/null +++ b/SecretAdmin/Features/Server/Utils.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using SecretAdmin.Features.Console; +using SecretAdmin.Features.Program; +using static SecretAdmin.Program; + +namespace SecretAdmin.Features.Server +{ + public static class Utils + { + public static bool GetExecutable(out string executable) + { + executable = "SCPSL.x86_64"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + executable = @"SCPSL.exe"; + +#if DEBUG + executable = @"C:\Program Files (x86)\Steam\steamapps\common\SCP Secret Laboratory Dedicated Server\SCPSL.exe"; +#endif + + if (File.Exists(executable)) + return true; + + Log.Alert("\nExecutable not found, make sure this file is on the same folder as LocalAdmin."); + return false; + } + + public static string GetLogsName(uint port) => Path.Combine(Paths.ServerLogsFolder, $"[{DateTime.Now:MM-dd-yyyy HH.mm}]-{port}.log"); + public static string GetOutputLogsName(uint port) => Path.Combine(Paths.ServerLogsFolder, $"[{DateTime.Now:MM-dd-yyyy HH.mm}]-{port}-output.log"); + + public static void ArchiveServerLogs() + { + var filesToArchive = (from fileName in Directory.GetFiles(Paths.ServerLogsFolder, "*.log") let reg = new Regex(@"\[(.*?)\]") let match = reg.Match(fileName) where match.Success && match.Groups[1].Value.Length > 15 && DateTime.TryParseExact(match.Groups[1].Value[..16], "MM-dd-yyyy HH.mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out var d) && d <= DateTime.Today.AddDays(-ConfigManager.SecretAdminConfig.ArchiveLogsDays) select fileName).ToList(); + var zipName = Path.Combine(Paths.ServerLogsFolder, $"{DateTime.Today.AddDays(-1):MM-dd-yyyy}-archive.zip"); + + if(!File.Exists(zipName)) + File.WriteAllText(zipName, ""); + + using var archive = ZipFile.Open(zipName, ZipArchiveMode.Update); + + foreach (var file in filesToArchive) + { + archive.CreateEntryFromFile(file, new FileInfo(file).Name); + File.Delete(file); + } + } + + public static void ArchiveControlLogs() + { + var filesToArchive = new List(); + foreach (var fileName in Directory.GetFiles(Paths.ProgramLogsFolder, "*.log")) + { + if (DateTime.TryParseExact(Path.GetFileName(fileName).Replace(".log", ""), "MM.dd.yyyy-hh.mm.ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var d) && d <= DateTime.Today.AddDays(-ConfigManager.SecretAdminConfig.ArchiveLogsDays)) + filesToArchive.Add(fileName); + } + + var zipName = Path.Combine(Paths.ProgramLogsFolder, $"{DateTime.Now:MM-dd-yyyy}-archive.zip"); + + if(!File.Exists(zipName)) + File.WriteAllText(zipName, ""); + + using var archive = ZipFile.Open(zipName, ZipArchiveMode.Update); + + foreach (var file in filesToArchive) + { + archive.CreateEntryFromFile(file, new FileInfo(file).Name); + File.Delete(file); + } + } + } +} \ No newline at end of file diff --git a/SecretAdmin/Program.cs b/SecretAdmin/Program.cs index 965e262..336d6c4 100644 --- a/SecretAdmin/Program.cs +++ b/SecretAdmin/Program.cs @@ -1,12 +1,11 @@ using System; -using System.Drawing; using System.IO; -using System.Threading.Tasks; +using SecretAdmin.API; using SecretAdmin.Features.Console; using SecretAdmin.Features.Program; +using SecretAdmin.Features.Program.Config; using SecretAdmin.Features.Server; using SecretAdmin.Features.Server.Commands; -using SecretAdmin.Features.Server.Enums; namespace SecretAdmin { @@ -14,27 +13,38 @@ class Program { public static Version Version { get; } = new (0, 0, 0,1); public static ScpServer Server { get; private set; } + public static ConfigManager ConfigManager { get; private set; } + public static Logger ProgramLogger { get; private set; } public static CommandHandler CommandHandler { get; private set; } static void Main(string[] args) { AppDomain.CurrentDomain.ProcessExit += OnExit; - Console.Title = $"SecretAdmin [v{Version}]"; - Log.Intro(); - Console.ReadKey(); + var arguments = ArgumentsManager.GetArgs(args); - if (ProgramIntroduction.FirstTime) + Paths.Load(); + ConfigManager = new ConfigManager(); + if (ProgramIntroduction.FirstTime || arguments.Reconfigure) ProgramIntroduction.ShowIntroduction(); + ConfigManager.LoadConfig(); + + if(arguments.Logs) + ProgramLogger = new Logger(Path.Combine(Paths.ProgramLogsFolder, $"{DateTime.Now:MM.dd.yyyy-hh.mm.ss}.log")); - Paths.Load(); + if(ConfigManager.SecretAdminConfig.AutoUpdater) + AutoUpdater.CheckForUpdates(); + Log.Intro(); + Utils.ArchiveControlLogs(); + CommandHandler = new CommandHandler(); + ModuleManager.LoadAll(); - Server = new ScpServer(7777); + Server = new ScpServer(ConfigManager.GetServerConfig(arguments.Config)); Server.Start(); - + InputManager.Start(); } @@ -43,8 +53,12 @@ private static void OnExit(object obj, EventArgs ev) Console.ForegroundColor = ConsoleColor.Blue; Console.WriteLine("\nExit Detected. Killing game process."); - Server?.Kill(); - + if (ConfigManager.SecretAdminConfig.SafeShutdown) + Server?.Kill(); + + foreach (var module in ModuleManager.Modules) + module.OnDisabled(); + Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine("Everything seems good to go! Bye :)"); } diff --git a/SecretAdmin/SecretAdmin.csproj b/SecretAdmin/SecretAdmin.csproj index 3278f83..47dbb93 100644 --- a/SecretAdmin/SecretAdmin.csproj +++ b/SecretAdmin/SecretAdmin.csproj @@ -10,4 +10,8 @@ + + + +