diff --git a/TGCommandLine/AdminCommands.cs b/TGCommandLine/AdminCommands.cs index 132ee6b395..611abd756d 100644 --- a/TGCommandLine/AdminCommands.cs +++ b/TGCommandLine/AdminCommands.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { diff --git a/TGCommandLine/BYONDCommands.cs b/TGCommandLine/BYONDCommands.cs index 1a0e9aa27d..3fed8adc7d 100644 --- a/TGCommandLine/BYONDCommands.cs +++ b/TGCommandLine/BYONDCommands.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { @@ -26,12 +27,12 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var type = TGByondVersion.Installed; + var type = ByondVersion.Installed; if (parameters.Count > 0) if (parameters[0].ToLower() == "--staged") - type = TGByondVersion.Staged; + type = ByondVersion.Staged; else if (parameters[0].ToLower() == "--latest") - type = TGByondVersion.Latest; + type = ByondVersion.Latest; OutputProc(Server.GetComponent().GetVersion(type) ?? "Unistalled"); return ExitCode.Normal; } @@ -57,22 +58,22 @@ namespace TGCommandLine { switch (Server.GetComponent().CurrentStatus()) { - case TGByondStatus.Downloading: + case ByondStatus.Downloading: OutputProc("Downloading update..."); break; - case TGByondStatus.Idle: + case ByondStatus.Idle: OutputProc("Updater Idle"); break; - case TGByondStatus.Staged: + case ByondStatus.Staged: OutputProc("Update staged and awaiting server restart"); break; - case TGByondStatus.Staging: + case ByondStatus.Staging: OutputProc("Staging update..."); break; - case TGByondStatus.Starting: + case ByondStatus.Starting: OutputProc("Starting update..."); break; - case TGByondStatus.Updating: + case ByondStatus.Updating: OutputProc("Applying update..."); break; default: @@ -117,13 +118,13 @@ namespace TGCommandLine } var stat = BYOND.CurrentStatus(); - while (stat != TGByondStatus.Idle && stat != TGByondStatus.Staged) + while (stat != ByondStatus.Idle && stat != ByondStatus.Staged) { Thread.Sleep(100); stat = BYOND.CurrentStatus(); } var res = BYOND.GetError(); - OutputProc(res ?? (stat == TGByondStatus.Staged ? "Update staged and will apply next DD reboot" : "Update finished")); + OutputProc(res ?? (stat == ByondStatus.Staged ? "Update staged and will apply next DD reboot" : "Update finished")); return res == null ? ExitCode.Normal : ExitCode.ServerError; } public override string GetArgumentString() diff --git a/TGCommandLine/ChatCommands.cs b/TGCommandLine/ChatCommands.cs index 0b262ef96b..9d53af175d 100644 --- a/TGCommandLine/ChatCommands.cs +++ b/TGCommandLine/ChatCommands.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { @@ -9,7 +10,7 @@ namespace TGCommandLine public IRCCommand() { Keyword = "irc"; - Children = new Command[] { new IRCNickCommand(), new IRCAuthCommand(), new IRCDisableAuthCommand(), new IRCServerCommand(), new ChatJoinCommand(TGChatProvider.IRC), new ChatPartCommand(TGChatProvider.IRC), new ChatListAdminsCommand(TGChatProvider.IRC), new ChatReconnectCommand(TGChatProvider.IRC), new ChatAddminCommand(TGChatProvider.IRC), new ChatDeadminCommand(TGChatProvider.IRC), new ChatEnableCommand(TGChatProvider.IRC), new ChatDisableCommand(TGChatProvider.IRC), new ChatStatusCommand(TGChatProvider.IRC), new IRCAuthModeCommand(), new IRCAuthLevelCommand() }; + Children = new Command[] { new IRCNickCommand(), new IRCAuthCommand(), new IRCDisableAuthCommand(), new IRCServerCommand(), new ChatJoinCommand(ChatProvider.IRC), new ChatPartCommand(ChatProvider.IRC), new ChatListAdminsCommand(ChatProvider.IRC), new ChatReconnectCommand(ChatProvider.IRC), new ChatAddminCommand(ChatProvider.IRC), new ChatDeadminCommand(ChatProvider.IRC), new ChatEnableCommand(ChatProvider.IRC), new ChatDisableCommand(ChatProvider.IRC), new ChatStatusCommand(ChatProvider.IRC), new IRCAuthModeCommand(), new IRCAuthLevelCommand() }; } public override string GetHelpText() { @@ -21,7 +22,7 @@ namespace TGCommandLine public DiscordCommand() { Keyword = "discord"; - Children = new Command[] { new DiscordSetTokenCommand(), new ChatJoinCommand(TGChatProvider.Discord), new ChatPartCommand(TGChatProvider.Discord), new ChatListAdminsCommand(TGChatProvider.Discord), new ChatReconnectCommand(TGChatProvider.Discord), new ChatAddminCommand(TGChatProvider.Discord), new ChatDeadminCommand(TGChatProvider.Discord), new ChatEnableCommand(TGChatProvider.Discord), new ChatDisableCommand(TGChatProvider.Discord), new ChatStatusCommand(TGChatProvider.Discord) , new DiscordAuthModeCommand() }; + Children = new Command[] { new DiscordSetTokenCommand(), new ChatJoinCommand(ChatProvider.Discord), new ChatPartCommand(ChatProvider.Discord), new ChatListAdminsCommand(ChatProvider.Discord), new ChatReconnectCommand(ChatProvider.Discord), new ChatAddminCommand(ChatProvider.Discord), new ChatDeadminCommand(ChatProvider.Discord), new ChatEnableCommand(ChatProvider.Discord), new ChatDisableCommand(ChatProvider.Discord), new ChatStatusCommand(ChatProvider.Discord) , new DiscordAuthModeCommand() }; } public override string GetHelpText() { @@ -48,7 +49,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { var Chat = Server.GetComponent(); - Chat.SetProviderInfo(new TGIRCSetupInfo(Chat.ProviderInfos()[(int)TGChatProvider.IRC]) + Chat.SetProviderInfo(new IRCSetupInfo(Chat.ProviderInfos()[(int)ChatProvider.IRC]) { Nickname = parameters[0], }); @@ -59,7 +60,7 @@ namespace TGCommandLine class ChatJoinCommand : Command { readonly int providerIndex; - public ChatJoinCommand(TGChatProvider pI) + public ChatJoinCommand(ChatProvider pI) { Keyword = "join"; RequiredParameters = 2; @@ -138,7 +139,7 @@ namespace TGCommandLine class ChatPartCommand : Command { readonly int providerIndex; - public ChatPartCommand(TGChatProvider pI) + public ChatPartCommand(ChatProvider pI) { Keyword = "part"; RequiredParameters = 2; @@ -178,7 +179,7 @@ namespace TGCommandLine return ExitCode.BadCommand; } var lowerParam = parameters[0].ToLower(); - if ((TGChatProvider)providerIndex == TGChatProvider.IRC && lowerParam[0] != '#') + if ((ChatProvider)providerIndex == ChatProvider.IRC && lowerParam[0] != '#') lowerParam = "#" + lowerParam; channels.Remove(lowerParam); switch (parameters[1].ToLower()) @@ -208,7 +209,7 @@ namespace TGCommandLine class ChatListAdminsCommand : Command { readonly int providerIndex; - public ChatListAdminsCommand(TGChatProvider pI) + public ChatListAdminsCommand(ChatProvider pI) { Keyword = "list-admins"; providerIndex = (int)pI; @@ -223,15 +224,15 @@ namespace TGCommandLine { var info = Server.GetComponent().ProviderInfos()[providerIndex]; string authType; - switch ((TGChatProvider)providerIndex) + switch ((ChatProvider)providerIndex) { - case TGChatProvider.IRC: + case ChatProvider.IRC: if (info.AdminsAreSpecial) authType = "Mode:"; else authType = "Nicknames:"; break; - case TGChatProvider.Discord: + case ChatProvider.Discord: if (info.AdminsAreSpecial) authType = "Role IDs:"; else @@ -242,8 +243,8 @@ namespace TGCommandLine return ExitCode.ServerError; } OutputProc("Authorized " + authType); - if (info.AdminsAreSpecial && (TGChatProvider)providerIndex == TGChatProvider.IRC) - switch(new TGIRCSetupInfo(info).AuthLevel) + if (info.AdminsAreSpecial && (ChatProvider)providerIndex == ChatProvider.IRC) + switch(new IRCSetupInfo(info).AuthLevel) { case IRCMode.Voice: OutputProc("+"); @@ -266,8 +267,8 @@ namespace TGCommandLine } class ChatReconnectCommand : Command { - readonly TGChatProvider providerIndex; - public ChatReconnectCommand(TGChatProvider pI) + readonly ChatProvider providerIndex; + public ChatReconnectCommand(ChatProvider pI) { Keyword = "reconnect"; providerIndex = pI; @@ -292,7 +293,7 @@ namespace TGCommandLine class ChatAddminCommand : Command { readonly int providerIndex; - public ChatAddminCommand(TGChatProvider pI) + public ChatAddminCommand(ChatProvider pI) { Keyword = "addmin"; RequiredParameters = 1; @@ -313,7 +314,7 @@ namespace TGCommandLine var info = IRC.ProviderInfos()[providerIndex]; var newmin = parameters[0].ToLower(); - if (info.AdminsAreSpecial && (TGChatProvider)providerIndex == TGChatProvider.IRC) + if (info.AdminsAreSpecial && (ChatProvider)providerIndex == ChatProvider.IRC) { OutputProc("Invalid auth mode for this command!"); return ExitCode.BadCommand; @@ -355,7 +356,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { var IRC = Server.GetComponent(); - var info = IRC.ProviderInfos()[(int)TGChatProvider.IRC]; + var info = IRC.ProviderInfos()[(int)ChatProvider.IRC]; var lowerparam = parameters[0].ToLower(); if (lowerparam == "channel-mode") info.AdminsAreSpecial = true; @@ -394,7 +395,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { var IRC = Server.GetComponent(); - var info = IRC.ProviderInfos()[(int)TGChatProvider.Discord]; + var info = IRC.ProviderInfos()[(int)ChatProvider.Discord]; var lowerparam = parameters[0].ToLower(); if (lowerparam == "role-id") info.AdminsAreSpecial = true; @@ -433,7 +434,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { var IRC = Server.GetComponent(); - var info = new TGIRCSetupInfo(IRC.ProviderInfos()[(int)TGChatProvider.IRC]); + var info = new IRCSetupInfo(IRC.ProviderInfos()[(int)ChatProvider.IRC]); switch (parameters[0]) { case "+": @@ -465,7 +466,7 @@ namespace TGCommandLine class ChatDeadminCommand : Command { readonly int providerIndex; - public ChatDeadminCommand(TGChatProvider pI) + public ChatDeadminCommand(ChatProvider pI) { Keyword = "deadmin"; RequiredParameters = 1; @@ -485,7 +486,7 @@ namespace TGCommandLine var info = IRC.ProviderInfos()[providerIndex]; var newmin = parameters[0].ToLower(); - if (info.AdminsAreSpecial && (TGChatProvider)providerIndex == TGChatProvider.IRC) + if (info.AdminsAreSpecial && (ChatProvider)providerIndex == ChatProvider.IRC) { OutputProc("Invalid auth mode for this command!"); return ExitCode.BadCommand; @@ -529,7 +530,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { var IRC = Server.GetComponent(); - IRC.SetProviderInfo(new TGIRCSetupInfo(IRC.ProviderInfos()[(int)TGChatProvider.IRC]) + IRC.SetProviderInfo(new IRCSetupInfo(IRC.ProviderInfos()[(int)ChatProvider.IRC]) { AuthTarget = parameters[0], AuthMessage = parameters[1] @@ -551,7 +552,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { var IRC = Server.GetComponent(); - IRC.SetProviderInfo(new TGIRCSetupInfo(IRC.ProviderInfos()[(int)TGChatProvider.IRC]) + IRC.SetProviderInfo(new IRCSetupInfo(IRC.ProviderInfos()[(int)ChatProvider.IRC]) { AuthTarget = null, AuthMessage = null, @@ -563,7 +564,7 @@ namespace TGCommandLine class ChatStatusCommand : Command { readonly int providerIndex; - public ChatStatusCommand(TGChatProvider pI) + public ChatStatusCommand(ChatProvider pI) { Keyword = "status"; providerIndex = (int)pI; @@ -596,7 +597,7 @@ namespace TGCommandLine class ChatEnableCommand : Command { readonly int providerIndex; - public ChatEnableCommand(TGChatProvider pI) + public ChatEnableCommand(ChatProvider pI) { Keyword = "enable"; providerIndex = (int)pI; @@ -624,7 +625,7 @@ namespace TGCommandLine class ChatDisableCommand : Command { readonly int providerIndex; - public ChatDisableCommand(TGChatProvider pI) + public ChatDisableCommand(ChatProvider pI) { Keyword = "disable"; providerIndex = (int)pI; @@ -675,7 +676,7 @@ namespace TGCommandLine return ExitCode.BadCommand; } var Chat = Server.GetComponent(); - var PI = new TGIRCSetupInfo(Chat.ProviderInfos()[(int)TGChatProvider.IRC]) + var PI = new IRCSetupInfo(Chat.ProviderInfos()[(int)ChatProvider.IRC]) { URL = splits[0] }; @@ -713,7 +714,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { var Chat = Server.GetComponent(); - var res = Chat.SetProviderInfo(new TGDiscordSetupInfo(Chat.ProviderInfos()[(int)TGChatProvider.Discord]) { BotToken = parameters[0] }); + var res = Chat.SetProviderInfo(new DiscordSetupInfo(Chat.ProviderInfos()[(int)ChatProvider.Discord]) { BotToken = parameters[0] }); OutputProc(res ?? "Success"); return res == null ? ExitCode.Normal : ExitCode.ServerError; } diff --git a/TGCommandLine/ConfigCommands.cs b/TGCommandLine/ConfigCommands.cs index ef2e3df3d9..6e4b3dd47f 100644 --- a/TGCommandLine/ConfigCommands.cs +++ b/TGCommandLine/ConfigCommands.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { diff --git a/TGCommandLine/DDCommands.cs b/TGCommandLine/DDCommands.cs index 8d263f12f5..6c754ed9a4 100644 --- a/TGCommandLine/DDCommands.cs +++ b/TGCommandLine/DDCommands.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { @@ -84,7 +85,7 @@ namespace TGCommandLine var DD = Server.GetComponent(); if (parameters.Count > 0 && parameters[0].ToLower() == "--graceful") { - if (DD.DaemonStatus() != TGDreamDaemonStatus.Online) + if (DD.DaemonStatus() != DreamDaemonStatus.Online) { OutputProc("Error: The game is not currently running!"); return ExitCode.ServerError; @@ -113,7 +114,7 @@ namespace TGCommandLine var DD = Server.GetComponent(); if (parameters.Count > 0 && parameters[0].ToLower() == "--graceful") { - if (DD.DaemonStatus() != TGDreamDaemonStatus.Online) + if (DD.DaemonStatus() != DreamDaemonStatus.Online) { OutputProc("Error: The game is not currently running!"); return ExitCode.ServerError; @@ -279,19 +280,19 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - TGDreamDaemonSecurity sec; + DreamDaemonSecurity sec; switch (parameters[0].ToLower()) { case "safe": - sec = TGDreamDaemonSecurity.Safe; + sec = DreamDaemonSecurity.Safe; break; case "ultra": case "ultrasafe": - sec = TGDreamDaemonSecurity.Ultrasafe; + sec = DreamDaemonSecurity.Ultrasafe; break; case "trust": case "trusted": - sec = TGDreamDaemonSecurity.Trusted; + sec = DreamDaemonSecurity.Trusted; break; default: OutputProc("Invalid security word!"); diff --git a/TGCommandLine/DMCommands.cs b/TGCommandLine/DMCommands.cs index f734e731f4..ca0c365448 100644 --- a/TGCommandLine/DMCommands.cs +++ b/TGCommandLine/DMCommands.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { @@ -29,13 +30,13 @@ namespace TGCommandLine { var DM = Server.GetComponent(); var stat = DM.GetStatus(); - if (stat != TGCompilerStatus.Initialized) + if (stat != CompilerStatus.Initialized) { - OutputProc("Error: Compiler is " + ((stat == TGCompilerStatus.Uninitialized) ? "unintialized!" : "busy with another task!")); + OutputProc("Error: Compiler is " + ((stat == CompilerStatus.Uninitialized) ? "unintialized!" : "busy with another task!")); return ExitCode.ServerError; } - if (Server.GetComponent().GetVersion(TGByondVersion.Installed) == null) + if (Server.GetComponent().GetVersion(ByondVersion.Installed) == null) { Console.Write("Error: BYOND is not installed!"); return ExitCode.ServerError; @@ -55,7 +56,7 @@ namespace TGCommandLine do { Thread.Sleep(1000); - } while (DM.GetStatus() == TGCompilerStatus.Compiling); + } while (DM.GetStatus() == CompilerStatus.Compiling); var res = DM.CompileError(); OutputProc(res ?? "Compilation successful"); if (res != null) @@ -95,17 +96,17 @@ namespace TGCommandLine Console.Write("Compilier is currently: "); switch (DM.GetStatus()) { - case TGCompilerStatus.Compiling: + case CompilerStatus.Compiling: OutputProc("Compiling..."); break; - case TGCompilerStatus.Initialized: + case CompilerStatus.Initialized: OutputProc("Idle"); ShowError(); break; - case TGCompilerStatus.Initializing: + case CompilerStatus.Initializing: OutputProc("Setting up..."); break; - case TGCompilerStatus.Uninitialized: + case CompilerStatus.Uninitialized: OutputProc("Uninitialized"); ShowError(); break; @@ -167,9 +168,9 @@ namespace TGCommandLine { var DM = Server.GetComponent(); var stat = DM.GetStatus(); - if (stat == TGCompilerStatus.Compiling || stat == TGCompilerStatus.Initializing) + if (stat == CompilerStatus.Compiling || stat == CompilerStatus.Initializing) { - OutputProc("Error: Compiler is " + ((stat == TGCompilerStatus.Initializing) ? "already initialized!" : " already running!")); + OutputProc("Error: Compiler is " + ((stat == CompilerStatus.Initializing) ? "already initialized!" : " already running!")); return ExitCode.ServerError; } if (!DM.Initialize()) @@ -186,7 +187,7 @@ namespace TGCommandLine do { Thread.Sleep(1000); - } while (DM.GetStatus() == TGCompilerStatus.Initializing); + } while (DM.GetStatus() == CompilerStatus.Initializing); var res = DM.CompileError(); OutputProc(res ?? "Initialization successful"); if (res != null) diff --git a/TGCommandLine/Program.cs b/TGCommandLine/Program.cs index 301da45f6c..adf4dcd154 100644 --- a/TGCommandLine/Program.cs +++ b/TGCommandLine/Program.cs @@ -2,13 +2,14 @@ using System.Collections.Generic; using System.Linq; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { class Program { - static ExitCode RunCommandLine(IList argsAsList) + static Command.ExitCode RunCommandLine(IList argsAsList) { //first lookup the connection string bool badConnectionString = false; @@ -58,7 +59,7 @@ namespace TGCommandLine if (badConnectionString) { Console.WriteLine("Remote connection usage: <-c/--connect> username:password@address:port"); - return ExitCode.BadCommand; + return Command.ExitCode.BadCommand; } var res = Server.VerifyConnection(); @@ -66,13 +67,13 @@ namespace TGCommandLine { Console.WriteLine("Unable to connect to service: " + res); Console.WriteLine("Remote connection usage: <-c/--connect> username:password@address:port"); - return ExitCode.ConnectionError; + return Command.ExitCode.ConnectionError; } if (!Server.Authenticate()) { Console.WriteLine("Authentication error: Username/password/windows identity is not authorized!"); - return ExitCode.ConnectionError; + return Command.ExitCode.ConnectionError; } if (!SentVMMWarning && Server.VersionMismatch(out string error)) @@ -88,7 +89,7 @@ namespace TGCommandLine catch (Exception e) { Console.WriteLine("Error: " + e.ToString()); - return ExitCode.ConnectionError; + return Command.ExitCode.ConnectionError; }; } public static string ReadLineSecure() @@ -208,11 +209,11 @@ namespace TGCommandLine break; case "quit": case "exit": - return (int)ExitCode.Normal; + return (int)Command.ExitCode.Normal; #if DEBUG case "debug-upgrade": Server.GetComponent().PrepareForUpdate(); - return (int)ExitCode.Normal; + return (int)Command.ExitCode.Normal; #endif default: //linq voodoo to get quoted strings diff --git a/TGCommandLine/RepoCommands.cs b/TGCommandLine/RepoCommands.cs index 1b0aa88599..e6c19d5930 100644 --- a/TGCommandLine/RepoCommands.cs +++ b/TGCommandLine/RepoCommands.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { diff --git a/TGCommandLine/RootCommands.cs b/TGCommandLine/RootCommands.cs index e70c88014c..5d7894d8db 100644 --- a/TGCommandLine/RootCommands.cs +++ b/TGCommandLine/RootCommands.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { diff --git a/TGControlPanel/ByondPage.cs b/TGControlPanel/ByondPage.cs index 62fccce395..3108172ef7 100644 --- a/TGControlPanel/ByondPage.cs +++ b/TGControlPanel/ByondPage.cs @@ -1,6 +1,7 @@ using System; using System.Windows.Forms; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGControlPanel { @@ -10,9 +11,9 @@ namespace TGControlPanel void InitBYONDPage() { var BYOND = Server.GetComponent(); - var CV = BYOND.GetVersion(TGByondVersion.Installed); + var CV = BYOND.GetVersion(ByondVersion.Installed); if (CV == null) - CV = BYOND.GetVersion(TGByondVersion.Staged); + CV = BYOND.GetVersion(ByondVersion.Staged); if (CV != null) { var splits = CV.Split('.'); @@ -29,7 +30,7 @@ namespace TGControlPanel } } - var latestVer = Server.GetComponent().GetVersion(TGByondVersion.Latest); + var latestVer = Server.GetComponent().GetVersion(ByondVersion.Latest); LatestVersionLabel.Text = latestVer; try @@ -58,33 +59,33 @@ namespace TGControlPanel { var BYOND = Server.GetComponent(); - VersionLabel.Text = BYOND.GetVersion(TGByondVersion.Installed) ?? "Not Installed"; + VersionLabel.Text = BYOND.GetVersion(ByondVersion.Installed) ?? "Not Installed"; StagedVersionTitle.Visible = false; StagedVersionLabel.Visible = false; switch (BYOND.CurrentStatus()) { - case TGByondStatus.Idle: - case TGByondStatus.Starting: + case ByondStatus.Idle: + case ByondStatus.Starting: StatusLabel.Text = "Idle"; UpdateButton.Enabled = true; break; - case TGByondStatus.Downloading: + case ByondStatus.Downloading: StatusLabel.Text = "Downloading..."; UpdateButton.Enabled = false; break; - case TGByondStatus.Staging: + case ByondStatus.Staging: StatusLabel.Text = "Staging..."; UpdateButton.Enabled = false; break; - case TGByondStatus.Staged: + case ByondStatus.Staged: StagedVersionTitle.Visible = true; StagedVersionLabel.Visible = true; - StagedVersionLabel.Text = BYOND.GetVersion(TGByondVersion.Staged) ?? "Unknown"; + StagedVersionLabel.Text = BYOND.GetVersion(ByondVersion.Staged) ?? "Unknown"; StatusLabel.Text = "Staged and waiting for BYOND to shutdown..."; UpdateButton.Enabled = true; break; - case TGByondStatus.Updating: + case ByondStatus.Updating: StatusLabel.Text = "Applying update..."; UpdateButton.Enabled = false; break; diff --git a/TGControlPanel/ChatPage.cs b/TGControlPanel/ChatPage.cs index 74eee5244d..f4f43c2229 100644 --- a/TGControlPanel/ChatPage.cs +++ b/TGControlPanel/ChatPage.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Windows.Forms; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGControlPanel { @@ -9,9 +10,9 @@ namespace TGControlPanel { bool updatingChat = false; - TGChatProvider ModifyingProvider + ChatProvider ModifyingProvider { - get { return (TGChatProvider)Properties.Settings.Default.LastChatProvider; } + get { return (ChatProvider)Properties.Settings.Default.LastChatProvider; } set { Properties.Settings.Default.LastChatProvider = (int)value; } } @@ -24,8 +25,8 @@ namespace TGControlPanel IRCModesComboBox.Visible = false; switch (ModifyingProvider) { - case TGChatProvider.Discord: - var DPI = new TGDiscordSetupInfo(PI); + case ChatProvider.Discord: + var DPI = new DiscordSetupInfo(PI); DiscordProviderSwitch.Select(); AuthField1.Text = DPI.BotToken; //it's invisible so whatever AuthField1Title.Text = "Bot Token:"; @@ -43,8 +44,8 @@ namespace TGControlPanel AdminModeNormal.Text = "User IDs"; AdminModeSpecial.Text = "Role IDs"; break; - case TGChatProvider.IRC: - var IRC = new TGIRCSetupInfo(PI); + case ChatProvider.IRC: + var IRC = new IRCSetupInfo(PI); IRCProviderSwitch.Select(); AuthField1.Text = IRC.AuthTarget; AuthField2.Text = IRC.AuthMessage; @@ -73,7 +74,7 @@ namespace TGControlPanel } break; default: - Properties.Settings.Default.LastChatProvider = (int)TGChatProvider.IRC; + Properties.Settings.Default.LastChatProvider = (int)ChatProvider.IRC; LoadChatPage(); return; } @@ -133,7 +134,7 @@ namespace TGControlPanel { if (!updatingChat && DiscordProviderSwitch.Checked) { - ModifyingProvider = TGChatProvider.Discord; + ModifyingProvider = ChatProvider.Discord; LoadChatPage(); } } @@ -142,7 +143,7 @@ namespace TGControlPanel { if (!updatingChat && IRCProviderSwitch.Checked) { - ModifyingProvider = TGChatProvider.IRC; + ModifyingProvider = ChatProvider.IRC; LoadChatPage(); } } @@ -172,17 +173,17 @@ namespace TGControlPanel private void ChatApplyButton_Click(object sender, EventArgs e) { string res = null; - TGChatSetupInfo wip = null; + ChatSetupInfo wip = null; switch (ModifyingProvider) { - case TGChatProvider.Discord: - wip = new TGDiscordSetupInfo() + case ChatProvider.Discord: + wip = new DiscordSetupInfo() { BotToken = AuthField1.Text }; break; - case TGChatProvider.IRC: - wip = new TGIRCSetupInfo() + case ChatProvider.IRC: + wip = new IRCSetupInfo() { AuthMessage = AuthField2.Text, AuthTarget = AuthField1.Text, diff --git a/TGControlPanel/RepoPage.cs b/TGControlPanel/RepoPage.cs index 15983de0ec..1191bc68da 100644 --- a/TGControlPanel/RepoPage.cs +++ b/TGControlPanel/RepoPage.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Windows.Forms; using System.Threading; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGControlPanel { diff --git a/TGControlPanel/ServerPage.cs b/TGControlPanel/ServerPage.cs index 41553f907a..acb9f0172a 100644 --- a/TGControlPanel/ServerPage.cs +++ b/TGControlPanel/ServerPage.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Windows.Forms; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGControlPanel { @@ -163,7 +164,7 @@ namespace TGControlPanel AutoUpdateInterval.Value = interval; var DaeStat = DD.DaemonStatus(); - var Online = DaeStat == TGDreamDaemonStatus.Online; + var Online = DaeStat == DreamDaemonStatus.Online; ServerStartButton.Enabled = !Online; ServerGStopButton.Enabled = Online; ServerGRestartButton.Enabled = Online; @@ -172,13 +173,13 @@ namespace TGControlPanel switch (DaeStat) { - case TGDreamDaemonStatus.HardRebooting: + case DreamDaemonStatus.HardRebooting: ServerStatusLabel.Text = "REBOOTING"; break; - case TGDreamDaemonStatus.Offline: + case DreamDaemonStatus.Offline: ServerStatusLabel.Text = "OFFLINE"; break; - case TGDreamDaemonStatus.Online: + case DreamDaemonStatus.Online: ServerStatusLabel.Text = "ONLINE"; var pc = DD.PlayerCount(); if (pc != -1) @@ -197,25 +198,25 @@ namespace TGControlPanel switch (DM.GetStatus()) { - case TGCompilerStatus.Compiling: + case CompilerStatus.Compiling: CompilerStatusLabel.Text = "Compiling..."; compileButton.Enabled = false; initializeButton.Enabled = false; CompileCancelButton.Enabled = true; break; - case TGCompilerStatus.Initializing: + case CompilerStatus.Initializing: CompilerStatusLabel.Text = "Initializing..."; compileButton.Enabled = false; initializeButton.Enabled = false; CompileCancelButton.Enabled = false; break; - case TGCompilerStatus.Initialized: + case CompilerStatus.Initialized: CompilerStatusLabel.Text = "Idle"; initializeButton.Enabled = true; compileButton.Enabled = true; CompileCancelButton.Enabled = false; break; - case TGCompilerStatus.Uninitialized: + case CompilerStatus.Uninitialized: CompilerStatusLabel.Text = "Uninitialized"; compileButton.Enabled = false; initializeButton.Enabled = true; @@ -459,7 +460,7 @@ namespace TGControlPanel private void SecuritySelector_SelectedIndexChanged(object sender, EventArgs e) { if (!updatingFields) - if (!Server.GetComponent().SetSecurityLevel((TGDreamDaemonSecurity)SecuritySelector.SelectedIndex)) + if (!Server.GetComponent().SetSecurityLevel((DreamDaemonSecurity)SecuritySelector.SelectedIndex)) MessageBox.Show("Security change will be applied after next server reboot."); } diff --git a/TGControlPanel/StaticPage.cs b/TGControlPanel/StaticPage.cs index 3f914afabc..e3b0003a2e 100644 --- a/TGControlPanel/StaticPage.cs +++ b/TGControlPanel/StaticPage.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Windows.Forms; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGControlPanel { diff --git a/TGInstallerWrapper/Main.cs b/TGInstallerWrapper/Main.cs index 828712ef55..79d8ac3081 100644 --- a/TGInstallerWrapper/Main.cs +++ b/TGInstallerWrapper/Main.cs @@ -17,8 +17,9 @@ namespace TGInstallerWrapper //reflection shit, make sure it matches const string InterfaceDLL = "TGServiceInterface.dll"; const string InterfaceNamespace = "TGServiceInterface"; + const string InterfaceComponentsNamespace = InterfaceNamespace + ".Components"; const string InterfaceClass = InterfaceNamespace + ".Server"; - const string InterfaceServiceInterface = InterfaceNamespace + ".ITGSService"; //fuck this typo + const string InterfaceServiceInterface = InterfaceComponentsNamespace + ".ITGSService"; //fuck this typo const string InterfaceClassVerifyConnection = "VerifyConnection"; const string InterfaceClassGetComponent = "GetComponent"; const string InterfaceServiceInterfaceVersion = "Version"; diff --git a/TGServerService/App.config b/TGServerService/App.config index 6a7a0786db..0d8567c88d 100644 --- a/TGServerService/App.config +++ b/TGServerService/App.config @@ -9,7 +9,7 @@ - + tgstation diff --git a/TGServerService/Chat.cs b/TGServerService/Chat.cs deleted file mode 100644 index 35e246c696..0000000000 --- a/TGServerService/Chat.cs +++ /dev/null @@ -1,294 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Web.Script.Serialization; -using TGServiceInterface; - -namespace TGServerService -{ - /// - /// Type of chat message, these may be OR'd together - /// - [Flags] - enum ChatMessageType - { - AdminInfo = 1, - GameInfo = 2, - WatchdogInfo = 4, - DeveloperInfo = 8, - } - interface ITGChatProvider : IDisposable - { - /// - /// Sets info for the provider - /// - /// The info to set - /// null on success, error message on failure - string SetProviderInfo(TGChatSetupInfo info); - - /// - /// Gets the info of the provider - /// - /// The info for the chat provider - TGChatSetupInfo ProviderInfo(); - - /// - /// Called with chat message info - /// - event OnChatMessage OnChatMessage; - - /// - /// Connects the chat provider if it's enabled - /// - /// null on success, error message on failure - string Connect(); - /// - /// Forces a reconnection of the chat provider if it's enabled - /// - /// null on success, error message on failure - string Reconnect(); - - /// - /// Checks if the chat provider is connected - /// - /// true if the provider is connected, false otherwise - bool Connected(); - - /// - /// Disconnects the chat provider - /// - void Disconnect(); - - /// - /// Send a message to a channel - /// - /// The message to send - /// The channel to send to - /// null on success, error message on failure - string SendMessageDirect(string message, string channel); - - /// - /// Broadcast a message to appropriate channels based on the message type - /// - /// The message to send - /// The message type - void SendMessage(string msg, ChatMessageType mt); - } - - /// - /// Callback for the chat provider recieving a message - /// - /// The chat provider the message came from - /// The username of the speaker - /// The name of the channel - /// The message text - /// if is considered a chat admin, otherwise - /// if is an admin chat channel, otherwise - - delegate void OnChatMessage(ITGChatProvider ChatProvider, string speaker, string channel, string message, bool isAdmin, bool isAdminChannel); - - partial class TGStationServer : ITGChat - { - - IList ChatProviders; - object ChatLock = new object(); - - public void InitChat() - { - var infos = InitProviderInfos(); - ChatProviders = new List(infos.Count); - foreach (var info in infos) - { - ITGChatProvider ChatProvider; - try - { - switch (info.Provider) - { - case TGChatProvider.Discord: - ChatProvider = new TGDiscordChatProvider(info); - break; - case TGChatProvider.IRC: - ChatProvider = new TGIRCChatProvider(info); - break; - default: - TGServerService.WriteError(String.Format("Invalid chat provider: {0}", info.Provider), TGServerService.EventID.InvalidChatProvider); - continue; - } - } - catch (Exception e) - { - TGServerService.WriteError(String.Format("Failed to start chat provider {0}! Error: {1}", info.Provider, e.ToString()), TGServerService.EventID.ChatProviderStartFail); - continue; - } - ChatProvider.OnChatMessage += ChatProvider_OnChatMessage; - var res = ChatProvider.Connect(); - if (res != null) - TGServerService.WriteWarning(String.Format("Unable to connect to chat! Provider {0}, Error: {1}", ChatProvider.GetType().ToString(), res), TGServerService.EventID.ChatConnectFail); - ChatProviders.Add(ChatProvider); - } - } - - private void ChatProvider_OnChatMessage(ITGChatProvider ChatProvider, string speaker, string channel, string message, bool isAdmin, bool isAdminChannel) - { - var splits = message.Trim().Split(' '); - - if (splits.Length == 1 && splits[0] == "") - { - ChatProvider.SendMessageDirect("Hi!", channel); - return; - } - - var asList = new List(splits); - - Command.OutputProcVar.Value = (m) => ChatProvider.SendMessageDirect(m, channel); - ChatCommand.CommandInfo.Value = new CommandInfo() - { - IsAdmin = isAdmin, - IsAdminChannel = isAdminChannel, - Speaker = speaker, - Server = this, - }; - TGServerService.WriteInfo(String.Format("Chat Command from {0} ({2}): {1}", speaker, String.Join(" ", asList), channel), TGServerService.EventID.ChatCommand); - if (ServerChatCommands == null) - LoadServerChatCommands(); - new RootChatCommand(ServerChatCommands).DoRun(asList); - } - - //cleanup and save - void DisposeChat() - { - var infosList = new List>(); - - foreach (var ChatProvider in ChatProviders) - { - infosList.Add(ChatProvider.ProviderInfo().DataFields); - ChatProvider.Dispose(); - } - ChatProviders = null; - - var rawdata = new JavaScriptSerializer().Serialize(infosList); - var Config = Properties.Settings.Default; - - Config.ChatProviderData = Helpers.EncryptData(rawdata, out string entrp); - Config.ChatProviderEntropy = entrp; - } - - public IList ProviderInfos() - { - var infosList = new List(); - foreach (var ChatProvider in ChatProviders) - infosList.Add(ChatProvider.ProviderInfo()); - return infosList; - } - - //public api - IList InitProviderInfos() - { - lock (ChatLock) - { - var Config = Properties.Settings.Default; - var rawdata = Config.ChatProviderData; - if (rawdata == "NEEDS INITIALIZING") - return new List() { new TGIRCSetupInfo(), new TGDiscordSetupInfo() }; - - string plaintext; - try - { - plaintext = Helpers.DecryptData(rawdata, Config.ChatProviderEntropy); - - var lists = new JavaScriptSerializer().Deserialize>>(plaintext); - var output = new List(lists.Count); - var foundirc = 0; - var founddiscord = 0; - foreach (var l in lists) - { - var info = new TGChatSetupInfo(l); - if (info.Provider == TGChatProvider.Discord) - ++founddiscord; - else if (info.Provider == TGChatProvider.IRC) - ++foundirc; - output.Add(info); - } - - if (foundirc != 1 || founddiscord != 1) - throw new Exception(); - - return output; - } - catch - { - Config.ChatProviderData = "NEEDS INITIALIZING"; - } - } - //if we get here we want to retry - return InitProviderInfos(); - } - - //public api - public string SetProviderInfo(TGChatSetupInfo info) - { - try - { - lock (ChatLock) - { - foreach (var ChatProvider in ChatProviders) - if (info.Provider == ChatProvider.ProviderInfo().Provider) - return ChatProvider.SetProviderInfo(info); - return "Error: Invalid provider: " + info.Provider.ToString(); - } - } - catch (Exception e) - { - return e.ToString(); - } - } - - //public api - public bool Connected(TGChatProvider providerType) - { - foreach (var I in ChatProviders) - if (I.ProviderInfo().Provider == providerType) - return I.Connected(); - return false; - } - - /// - /// Reconnect servers that are enabled and disconnected - /// - void ChatConnectivityCheck() - { - foreach (TGChatProvider I in Enum.GetValues(typeof(TGChatProvider))) - if(!Connected(I)) - Reconnect(I); - } - - //public api - public string Reconnect(TGChatProvider providerType) - { - foreach (var I in ChatProviders) - if (I.ProviderInfo().Provider == providerType) - return I.Reconnect(); - return "Could not find specified provider!"; - } - - /// - /// Broadcast a message to appropriate channels based on the message type - /// - /// The message to send - /// The message type - public void SendMessage(string msg, ChatMessageType mt) - { - lock (ChatLock) - { - foreach (var ChatProvider in ChatProviders) - try - { - ChatProvider.SendMessage(msg, mt); - }catch(Exception e) - { - TGServerService.WriteWarning(String.Format("Chat broadcast failed (Provider: {3}) (Flags: {0}) (Message: {1}): {2}", mt, msg, e.ToString(), ChatProvider.ProviderInfo().Provider), TGServerService.EventID.ChatBroadcastFail); - } - } - } - } -} diff --git a/TGServerService/ChatCommands.cs b/TGServerService/ChatCommands.cs index 0f6db947b7..fca74decb2 100644 --- a/TGServerService/ChatCommands.cs +++ b/TGServerService/ChatCommands.cs @@ -3,20 +3,20 @@ using System; using System.Collections.Generic; using System.Threading; -namespace TGServerService +namespace TGServerService.ChatCommands { class CommandInfo { public bool IsAdmin { get; set; } public bool IsAdminChannel { get; set; } public string Speaker { get; set; } - public TGStationServer Server { get; set; } + public ServerInstance Server { get; set; } } abstract class ChatCommand : Command { public bool RequiresAdmin { get; protected set; } public static ThreadLocal CommandInfo = new ThreadLocal(); - protected TGStationServer Instance { get { return CommandInfo.Value.Server; } } + protected ServerInstance Instance { get { return CommandInfo.Value.Server; } } public override ExitCode DoRun(IList parameters) { if (RequiresAdmin) @@ -55,7 +55,7 @@ namespace TGServerService protected override ExitCode Run(IList parameters) { - var res = Instance.SendCommand(String.Format("{0};sender={1};custom={2}", Keyword, CommandInfo.Value.Speaker, TGStationServer.SanitizeTopicString(String.Join(" ", parameters)))); + var res = Instance.SendCommand(String.Format("{0};sender={1};custom={2}", Keyword, CommandInfo.Value.Speaker, Program.SanitizeTopicString(String.Join(" ", parameters)))); if (res != "SUCCESS" && !String.IsNullOrWhiteSpace(res)) OutputProc(res); return ExitCode.Normal; @@ -105,12 +105,12 @@ namespace TGServerService } protected override ExitCode Run(IList parameters) { - var type = TGByondVersion.Installed; + var type = ByondVersion.Installed; if (parameters.Count > 0) if (parameters[0].ToLower() == "--staged") - type = TGByondVersion.Staged; + type = ByondVersion.Staged; else if (parameters[0].ToLower() == "--latest") - type = TGByondVersion.Latest; + type = ByondVersion.Latest; OutputProc(Instance.GetVersion(type) ?? "None"); return ExitCode.Normal; } diff --git a/TGServerService/ChatMessageType.cs b/TGServerService/ChatMessageType.cs new file mode 100644 index 0000000000..a727a28488 --- /dev/null +++ b/TGServerService/ChatMessageType.cs @@ -0,0 +1,28 @@ +using System; + +namespace TGServerService +{ + /// + /// Type of chat message, these may be OR'd together + /// + [Flags] + enum ChatMessageType + { + /// + /// Send message to the admin channels + /// + AdminInfo = 1, + /// + /// Send message to the game channels + /// + GameInfo = 2, + /// + /// Send message to the watchdog channels + /// + WatchdogInfo = 4, + /// + /// Send message to the coder channels + /// + DeveloperInfo = 8, + } +} diff --git a/TGServerService/ChatProviders/ChatProvider.cs b/TGServerService/ChatProviders/ChatProvider.cs new file mode 100644 index 0000000000..e343f2b79b --- /dev/null +++ b/TGServerService/ChatProviders/ChatProvider.cs @@ -0,0 +1,77 @@ +using System; +using TGServiceInterface; + +namespace TGServerService.ChatProviders +{ + /// + /// Callback for the chat provider recieving a message + /// + /// The chat provider the message came from + /// The username of the speaker + /// The name of the channel + /// The message text + /// if is considered a chat admin, otherwise + /// if is an admin chat channel, otherwise + + delegate void OnChatMessage(ITGChatProvider ChatProvider, string speaker, string channel, string message, bool isAdmin, bool isAdminChannel); + /// + /// Interface for a chat provder service + /// + interface ITGChatProvider : IDisposable + { + /// + /// Sets info for the provider + /// + /// The info to set + /// null on success, error message on failure + string SetProviderInfo(ChatSetupInfo info); + + /// + /// Gets the info of the provider + /// + /// The info for the chat provider + ChatSetupInfo ProviderInfo(); + + /// + /// Called with chat message info + /// + event OnChatMessage OnChatMessage; + + /// + /// Connects the chat provider if it's enabled + /// + /// null on success, error message on failure + string Connect(); + /// + /// Forces a reconnection of the chat provider if it's enabled + /// + /// null on success, error message on failure + string Reconnect(); + + /// + /// Checks if the chat provider is connected + /// + /// true if the provider is connected, false otherwise + bool Connected(); + + /// + /// Disconnects the chat provider + /// + void Disconnect(); + + /// + /// Send a message to a channel + /// + /// The message to send + /// The channel to send to + /// null on success, error message on failure + string SendMessageDirect(string message, string channel); + + /// + /// Broadcast a message to appropriate channels based on the message type + /// + /// The message to send + /// The message type + void SendMessage(string msg, ChatMessageType mt); + } +} diff --git a/TGServerService/Discord.cs b/TGServerService/ChatProviders/DiscordChatProvider.cs similarity index 90% rename from TGServerService/Discord.cs rename to TGServerService/ChatProviders/DiscordChatProvider.cs index 87758d74e5..573ef7517e 100644 --- a/TGServerService/Discord.cs +++ b/TGServerService/ChatProviders/DiscordChatProvider.cs @@ -6,30 +6,30 @@ using System.Threading; using System.Threading.Tasks; using TGServiceInterface; -namespace TGServerService +namespace TGServerService.ChatProviders { - class TGDiscordChatProvider : ITGChatProvider + class DiscordChatProvider : ITGChatProvider { public event OnChatMessage OnChatMessage; DiscordSocketClient client; - TGDiscordSetupInfo DiscordConfig; + DiscordSetupInfo DiscordConfig; object DiscordLock = new object(); IDictionary SeenPrivateChannels = new Dictionary(); - public TGDiscordChatProvider(TGChatSetupInfo info) + public DiscordChatProvider(ChatSetupInfo info) { Init(info); } - public TGChatSetupInfo ProviderInfo() + public ChatSetupInfo ProviderInfo() { return DiscordConfig; } - void Init(TGChatSetupInfo info) + void Init(ChatSetupInfo info) { - DiscordConfig = new TGDiscordSetupInfo(info); + DiscordConfig = new DiscordSetupInfo(info); client = new DiscordSocketClient(); client.MessageReceived += Client_MessageReceived; } @@ -176,7 +176,7 @@ namespace TGServerService foreach (var J in I.TextChannels) if (J.Id == channel) J.SendMessageAsync(message).Wait(); - TGServerService.WriteInfo(String.Format("Discord Send ({0}): {1}", channelname, message), TGServerService.EventID.ChatSend); + Service.WriteInfo(String.Format("Discord Send ({0}): {1}", channelname, message), EventID.ChatSend); return null; } } @@ -193,19 +193,19 @@ namespace TGServerService client.LogoutAsync().Wait(); } catch (Exception e) { - TGServerService.WriteError("Discord failed DnD: " + e.ToString(), TGServerService.EventID.ChatDisconnectFail); + Service.WriteError("Discord failed DnD: " + e.ToString(), EventID.ChatDisconnectFail); } client.Dispose(); } - public string SetProviderInfo(TGChatSetupInfo info) + public string SetProviderInfo(ChatSetupInfo info) { try { lock (DiscordLock) { var odc = DiscordConfig; - DiscordConfig = new TGDiscordSetupInfo(info); + DiscordConfig = new DiscordSetupInfo(info); if (DiscordConfig.BotToken != odc.BotToken) { DisconnectAndDispose(); diff --git a/TGServerService/IRC.cs b/TGServerService/ChatProviders/IRCChatProvider.cs similarity index 92% rename from TGServerService/IRC.cs rename to TGServerService/ChatProviders/IRCChatProvider.cs index a04dfc2d36..279aee4323 100644 --- a/TGServerService/IRC.cs +++ b/TGServerService/ChatProviders/IRCChatProvider.cs @@ -5,31 +5,31 @@ using TGServiceInterface; using Meebey.SmartIrc4net; -namespace TGServerService +namespace TGServerService.ChatProviders { - class TGIRCChatProvider : ITGChatProvider + class IRCChatProvider : ITGChatProvider { const string PrivateMessageMarker = "---PRIVATE-MSG---"; IrcFeatures irc; object IRCLock = new object(); - TGIRCSetupInfo IRCConfig; + IRCSetupInfo IRCConfig; public event OnChatMessage OnChatMessage; - public TGChatSetupInfo ProviderInfo() + public ChatSetupInfo ProviderInfo() { return IRCConfig; } - public TGIRCChatProvider(TGChatSetupInfo info) + public IRCChatProvider(ChatSetupInfo info) { - IRCConfig = new TGIRCSetupInfo(info); + IRCConfig = new IRCSetupInfo(info); irc = new IrcFeatures() { SupportNonRfc = true, - CtcpUserInfo = TGServerService.Version, + CtcpUserInfo = Service.Version, AutoRejoin = true, AutoRejoinOnKick = true, AutoRelogin = true, @@ -55,7 +55,7 @@ namespace TGServerService channel = channel.Replace(PrivateMessageMarker, ""); irc.SendMessage(SendType.Message, channel, message); } - TGServerService.WriteInfo(String.Format("IRC Send ({0}): {1}", channel, message), TGServerService.EventID.ChatSend); + Service.WriteInfo(String.Format("IRC Send ({0}): {1}", channel, message), EventID.ChatSend); return null; } catch (Exception e) @@ -64,9 +64,9 @@ namespace TGServerService } } - public string SetProviderInfo(TGChatSetupInfo info) + public string SetProviderInfo(ChatSetupInfo info) { - var convertedInfo = (TGIRCSetupInfo)info; + var convertedInfo = (IRCSetupInfo)info; var serverChange = convertedInfo.URL != IRCConfig.URL || convertedInfo.Port != IRCConfig.Port; IRCConfig = convertedInfo; if (!IRCConfig.Enabled) @@ -247,7 +247,7 @@ namespace TGServerService } catch (Exception e) { - TGServerService.WriteError("IRC failed QnD: " + e.ToString(), TGServerService.EventID.ChatDisconnectFail); + Service.WriteError("IRC failed QnD: " + e.ToString(), EventID.ChatDisconnectFail); } } //public api diff --git a/TGServerService/EventID.cs b/TGServerService/EventID.cs new file mode 100644 index 0000000000..8f83de9e38 --- /dev/null +++ b/TGServerService/EventID.cs @@ -0,0 +1,78 @@ +namespace TGServerService +{ + enum EventID + { + ChatCommand = 100, + ChatConnectFail = 200, + ChatProviderStartFail = 300, + InvalidChatProvider = 400, + UpdateRequest = 500, + BYONDUpdateFail = 600, + BYONDUpdateStaged = 700, + BYONDUpdateComplete = 800, + ServerMoveFailed = 900, + ServerMovePartial = 1000, + ServerMoveComplete = 1100, + DMCompileCrash = 1200, + DMInitializeCrash = 1300, + DMCompileError = 1400, + DMCompileSuccess = 1500, + DMCompileCancel = 1600, + DDReattachFail = 1700, + DDReattachSuccess = 1800, + DDWatchdogCrash = 1900, + DDWatchdogExit = 2000, + DDWatchdogRebootedServer = 2100, + DDWatchdogRebootingServer = 2200, + DDWatchdogRestart = 2300, + DDWatchdogRestarted = 2400, + DDWatchdogStarted = 2500, + ChatSend = 2600, + ChatBroadcast = 2700, + //ChatAdminBroadcast = 2800, + ChatDisconnectFail = 2900, + //TopicSent = 3000, + //TopicFailed = 3100, + CommsKeySet = 3200, + NudgeStartFail = 3300, + NudgeCrash = 3400, + RepoClone = 3500, + RepoCloneFail = 3600, + RepoCheckout = 3700, + RepoCheckoutFail = 3800, + RepoHardUpdate = 3900, + RepoHardUpdateFail = 4000, + RepoMergeUpdate = 4100, + RepoMergeUpdateFail = 4200, + RepoBackupTag = 4300, + RepoBackupTagFail = 4400, + RepoResetTracked = 4500, + RepoResetTrackedFail = 4600, + RepoReset = 4700, + RepoResetFail = 4800, + RepoPRListError = 4900, + RepoPRMerge = 5000, + RepoPRMergeFail = 5100, + RepoCommit = 5200, + RepoCommitFail = 5300, + RepoPush = 5400, + RepoPushFail = 5500, + RepoChangelog = 5600, + RepoChangelogFail = 5700, + ServiceShutdownFail = 6100, + WorldReboot = 6200, + ServerUpdateApplied = 6300, + ChatBroadcastFail = 6400, + IRCLogModes = 6500, + SubmoduleReclone = 6600, + Authentication = 6700, + PreactionEvent = 6800, + PreactionFail = 6900, + InteropCallException = 7000, + APIVersionMismatch = 7100, + RepoConfigurationFail = 7200, + StaticRead = 7300, + StaticWrite = 7400, + StaticDelete = 7500, + } +} diff --git a/TGServerService/Program.cs b/TGServerService/Program.cs index 3a03de02f0..38f5bae952 100644 --- a/TGServerService/Program.cs +++ b/TGServerService/Program.cs @@ -6,7 +6,7 @@ namespace TGServerService { static class Program { - static void Main() => new TGServerService(); //wondows + static void Main() => new Service(); //wondows //Everything in this file is just generic helpers @@ -101,5 +101,10 @@ namespace TGServerService CopyDirectory(subdir.FullName, temppath, ignore); } } + + public static string SanitizeTopicString(string input) + { + return input.Replace("%", "%25").Replace("=", "%3d").Replace(";", "%3b").Replace("&", "%26").Replace("+", "%2b"); + } } } diff --git a/TGServerService/ProjectInstaller.Designer.cs b/TGServerService/ProjectInstaller.Designer.cs index 1132b0a780..0bbf4b1e54 100644 --- a/TGServerService/ProjectInstaller.Designer.cs +++ b/TGServerService/ProjectInstaller.Designer.cs @@ -1,4 +1,4 @@ -namespace ServerService +namespace TGServerService { partial class ProjectInstaller { diff --git a/TGServerService/ProjectInstaller.cs b/TGServerService/ProjectInstaller.cs index 0580a2c6b7..ffc4df84b6 100644 --- a/TGServerService/ProjectInstaller.cs +++ b/TGServerService/ProjectInstaller.cs @@ -1,7 +1,7 @@ using System.ComponentModel; using System.Configuration.Install; -namespace ServerService +namespace TGServerService { [RunInstaller(true)] partial class ProjectInstaller : Installer diff --git a/TGServerService/Administration.cs b/TGServerService/ServerInstance/Administration.cs similarity index 87% rename from TGServerService/Administration.cs rename to TGServerService/ServerInstance/Administration.cs index cc43d818e9..3baf5d6b05 100644 --- a/TGServerService/Administration.cs +++ b/TGServerService/ServerInstance/Administration.cs @@ -5,12 +5,13 @@ using System.Security.Principal; using System.ServiceModel; using System.Threading; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGServerService { //note this only works with MACHINE LOCAL groups and admins for now //if someone wants AD shit, code it yourself - partial class TGStationServer : ServiceAuthorizationManager, ITGAdministration + partial class ServerInstance : ServiceAuthorizationManager, ITGAdministration { SecurityIdentifier TheDroidsWereLookingFor; object authLock = new object(); @@ -103,7 +104,7 @@ namespace TGServerService if (LastSeenUser != user) { LastSeenUser = user; - TGServerService.WriteAccess(user, authSuccess); + Service.WriteAccess(user, authSuccess); } } return authSuccess; @@ -159,20 +160,20 @@ namespace TGServerService return "BYOND locked"; try { - if (updateStat != TGByondStatus.Idle) + if (updateStat != ByondStatus.Idle) return "BYOND busy!"; if (!Monitor.TryEnter(CompilerLock)) return "Compiler locked!"; try { - if (compilerCurrentStatus != TGCompilerStatus.Uninitialized && compilerCurrentStatus != TGCompilerStatus.Initialized) + if (compilerCurrentStatus != CompilerStatus.Uninitialized && compilerCurrentStatus != CompilerStatus.Initialized) return "Compiler busy!"; if (!Monitor.TryEnter(watchdogLock)) return "Watchdog locked!"; try { - if (currentStatus != TGDreamDaemonStatus.Offline) + if (currentStatus != DreamDaemonStatus.Offline) return "Watchdog running!"; lock (configLock) { @@ -191,7 +192,7 @@ namespace TGServerService catch (Exception e) { error = "The move was successful, but the path " + Config.ServerDirectory + " was unable to be deleted fully!"; - TGServerService.WriteWarning(String.Format("Server move from {0} to {1} partial success: {2}", Config.ServerDirectory, new_location, e.ToString()), TGServerService.EventID.ServerMovePartial); + Service.WriteWarning(String.Format("Server move from {0} to {1} partial success: {2}", Config.ServerDirectory, new_location, e.ToString()), EventID.ServerMovePartial); } } else @@ -208,7 +209,7 @@ namespace TGServerService throw; } } - TGServerService.WriteInfo(String.Format("Server moved from {0} to {1}", Config.ServerDirectory, new_location), TGServerService.EventID.ServerMoveComplete); + Service.WriteInfo(String.Format("Server moved from {0} to {1}", Config.ServerDirectory, new_location), EventID.ServerMoveComplete); Config.ServerDirectory = new_location; return null; } @@ -235,7 +236,7 @@ namespace TGServerService } catch (Exception e) { - TGServerService.WriteError(String.Format("Server move from {0} to {1} failed: {2}", Config.ServerDirectory, new_location, e.ToString()), TGServerService.EventID.ServerMoveFailed); + Service.WriteError(String.Format("Server move from {0} to {1} failed: {2}", Config.ServerDirectory, new_location, e.ToString()), EventID.ServerMoveFailed); return e.ToString(); } } @@ -253,7 +254,7 @@ namespace TGServerService return "Static dir locked!"; try { - if (currentStatus != TGDreamDaemonStatus.Offline) + if (currentStatus != DreamDaemonStatus.Offline) return "Watchdog running!"; BackupAndDeleteStaticDirectory(); InitialConfigureRepository(); diff --git a/TGServerService/Byond.cs b/TGServerService/ServerInstance/Byond.cs similarity index 78% rename from TGServerService/Byond.cs rename to TGServerService/ServerInstance/Byond.cs index e8c4646fab..e3e415649e 100644 --- a/TGServerService/Byond.cs +++ b/TGServerService/ServerInstance/Byond.cs @@ -1,16 +1,16 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Net; using System.Text.RegularExpressions; using System.Threading; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGServerService { - partial class TGStationServer : ITGByond + partial class ServerInstance : ITGByond { const string ByondDirectory = "BYOND"; const string StagingDirectory = "BYOND_staged"; @@ -24,7 +24,7 @@ namespace TGServerService const string ByondDDConfig = "/daemon.txt"; const string ByondNoPromptTrustedMode = "trusted-check 0"; - TGByondStatus updateStat = TGByondStatus.Idle; + ByondStatus updateStat = ByondStatus.Idle; object ByondLock = new object(); string lastError; @@ -61,19 +61,19 @@ namespace TGServerService switch (updateStat) { default: - case TGByondStatus.Starting: - case TGByondStatus.Downloading: - case TGByondStatus.Staging: - case TGByondStatus.Updating: + case ByondStatus.Starting: + case ByondStatus.Downloading: + case ByondStatus.Staging: + case ByondStatus.Updating: return true; - case TGByondStatus.Idle: - case TGByondStatus.Staged: + case ByondStatus.Idle: + case ByondStatus.Staged: return false; } } //public api - public TGByondStatus CurrentStatus() + public ByondStatus CurrentStatus() { lock (ByondLock) { @@ -93,13 +93,13 @@ namespace TGServerService } //public api - public string GetVersion(TGByondVersion type) + public string GetVersion(ByondVersion type) { try { lock (ByondLock) { - if (type == TGByondVersion.Latest) + if (type == ByondVersion.Latest) { //get the latest version from the website HttpWebRequest request = (HttpWebRequest)WebRequest.Create(ByondLatestURL); @@ -123,7 +123,7 @@ namespace TGServerService } else { - string DirToUse = type == TGByondVersion.Staged ? StagingDirectoryInner : ByondDirectory; + string DirToUse = type == ByondVersion.Staged ? StagingDirectoryInner : ByondDirectory; if (Directory.Exists(DirToUse)) { string file = DirToUse + VersionFile; @@ -151,9 +151,9 @@ namespace TGServerService public void UpdateToVersionImpl(object param) { lock (ByondLock) { - if (updateStat != TGByondStatus.Starting) + if (updateStat != ByondStatus.Starting) return; - updateStat = TGByondStatus.Downloading; + updateStat = ByondStatus.Downloading; } try @@ -175,17 +175,17 @@ namespace TGServerService { SendMessage("BYOND: Update download failed. Does the specified version exist?", ChatMessageType.DeveloperInfo); lastError = String.Format("Download of BYOND version {0}.{1} failed! Does it exist?", vi.major, vi.minor); - TGServerService.WriteWarning(String.Format("Failed to update BYOND to version {0}.{1}!", vi.major, vi.minor), TGServerService.EventID.BYONDUpdateFail); + Service.WriteWarning(String.Format("Failed to update BYOND to version {0}.{1}!", vi.major, vi.minor), EventID.BYONDUpdateFail); lock (ByondLock) { - updateStat = TGByondStatus.Idle; + updateStat = ByondStatus.Idle; } return; } } lock (ByondLock) { - updateStat = TGByondStatus.Staging; + updateStat = ByondStatus.Staging; } //STAGING @@ -202,12 +202,12 @@ namespace TGServerService lock (ByondLock) { - updateStat = TGByondStatus.Staged; + updateStat = ByondStatus.Staged; } switch (DaemonStatus()) { - case TGDreamDaemonStatus.Offline: + case DreamDaemonStatus.Offline: if(ApplyStagedUpdate()) lastError = null; else @@ -217,7 +217,7 @@ namespace TGServerService RequestRestart(); lastError = "Update staged. Awaiting server restart..."; SendMessage(String.Format("BYOND: Staging complete. Awaiting server restart...", vi.major, vi.minor), ChatMessageType.DeveloperInfo); - TGServerService.WriteInfo(String.Format("BYOND update {0}.{1} staged", vi.major, vi.minor), TGServerService.EventID.BYONDUpdateStaged); + Service.WriteInfo(String.Format("BYOND update {0}.{1} staged", vi.major, vi.minor), EventID.BYONDUpdateStaged); break; } } @@ -227,10 +227,10 @@ namespace TGServerService } catch (Exception e) { - TGServerService.WriteError("Revision staging errror: " + e.ToString(), TGServerService.EventID.BYONDUpdateFail); + Service.WriteError("Revision staging errror: " + e.ToString(), EventID.BYONDUpdateFail); lock (ByondLock) { - updateStat = TGByondStatus.Idle; + updateStat = ByondStatus.Idle; lastError = e.ToString(); RevisionStaging = null; } @@ -243,7 +243,7 @@ namespace TGServerService { if (!BusyCheckNoLock()) { - updateStat = TGByondStatus.Starting; + updateStat = ByondStatus.Starting; RevisionStaging = new Thread(new ParameterizedThreadStart(UpdateToVersionImpl)) { IsBackground = true //don't slow me down @@ -259,13 +259,13 @@ namespace TGServerService { lock (CompilerLock) { - if (compilerCurrentStatus == TGCompilerStatus.Compiling) + if (compilerCurrentStatus == CompilerStatus.Compiling) return false; lock (ByondLock) { - if (updateStat != TGByondStatus.Staged) + if (updateStat != ByondStatus.Staged) return false; - updateStat = TGByondStatus.Updating; + updateStat = ByondStatus.Updating; } try { @@ -274,20 +274,20 @@ namespace TGServerService Program.DeleteDirectory(StagingDirectory); lastError = null; SendMessage("BYOND: Update completed!", ChatMessageType.DeveloperInfo); - TGServerService.WriteInfo(String.Format("BYOND update {0} completed!", GetVersion(TGByondVersion.Installed)), TGServerService.EventID.BYONDUpdateComplete); + Service.WriteInfo(String.Format("BYOND update {0} completed!", GetVersion(ByondVersion.Installed)), EventID.BYONDUpdateComplete); return true; } catch (Exception e) { lastError = e.ToString(); SendMessage("BYOND: Update failed!", ChatMessageType.DeveloperInfo); - TGServerService.WriteError("BYOND update failed!", TGServerService.EventID.BYONDUpdateFail); + Service.WriteError("BYOND update failed!", EventID.BYONDUpdateFail); return false; } finally { lock(ByondLock) { - updateStat = TGByondStatus.Idle; + updateStat = ByondStatus.Idle; } } } diff --git a/TGServerService/ServerInstance/Chat.cs b/TGServerService/ServerInstance/Chat.cs new file mode 100644 index 0000000000..d017dab391 --- /dev/null +++ b/TGServerService/ServerInstance/Chat.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using TGServerService.ChatCommands; +using TGServerService.ChatProviders; +using TGServiceInterface; +using TGServiceInterface.Components; + +namespace TGServerService +{ + partial class ServerInstance : ITGChat + { + + IList ChatProviders; + object ChatLock = new object(); + + public void InitChat() + { + var infos = InitProviderInfos(); + ChatProviders = new List(infos.Count); + foreach (var info in infos) + { + ITGChatProvider chatProvider; + try + { + switch (info.Provider) + { + case ChatProvider.Discord: + chatProvider = new DiscordChatProvider(info); + break; + case ChatProvider.IRC: + chatProvider = new IRCChatProvider(info); + break; + default: + Service.WriteError(String.Format("Invalid chat provider: {0}", info.Provider), EventID.InvalidChatProvider); + continue; + } + } + catch (Exception e) + { + Service.WriteError(String.Format("Failed to start chat provider {0}! Error: {1}", info.Provider, e.ToString()), EventID.ChatProviderStartFail); + continue; + } + chatProvider.OnChatMessage += ChatProvider_OnChatMessage; + var res = chatProvider.Connect(); + if (res != null) + Service.WriteWarning(String.Format("Unable to connect to chat! Provider {0}, Error: {1}", chatProvider.GetType().ToString(), res), EventID.ChatConnectFail); + ChatProviders.Add(chatProvider); + } + } + + private void ChatProvider_OnChatMessage(ITGChatProvider ChatProvider, string speaker, string channel, string message, bool isAdmin, bool isAdminChannel) + { + var splits = message.Trim().Split(' '); + + if (splits.Length == 1 && splits[0] == "") + { + ChatProvider.SendMessageDirect("Hi!", channel); + return; + } + + var asList = new List(splits); + + Command.OutputProcVar.Value = (m) => ChatProvider.SendMessageDirect(m, channel); + ChatCommand.CommandInfo.Value = new CommandInfo() + { + IsAdmin = isAdmin, + IsAdminChannel = isAdminChannel, + Speaker = speaker, + Server = this, + }; + Service.WriteInfo(String.Format("Chat Command from {0} ({2}): {1}", speaker, String.Join(" ", asList), channel), EventID.ChatCommand); + if (ServerChatCommands == null) + LoadServerChatCommands(); + new RootChatCommand(ServerChatCommands).DoRun(asList); + } + + //cleanup and save + void DisposeChat() + { + var infosList = new List>(); + + foreach (var ChatProvider in ChatProviders) + { + infosList.Add(ChatProvider.ProviderInfo().DataFields); + ChatProvider.Dispose(); + } + ChatProviders = null; + + var rawdata = new JavaScriptSerializer().Serialize(infosList); + var Config = Properties.Settings.Default; + + Config.ChatProviderData = Helpers.EncryptData(rawdata, out string entrp); + Config.ChatProviderEntropy = entrp; + } + + public IList ProviderInfos() + { + var infosList = new List(); + foreach (var chatProvider in ChatProviders) + infosList.Add(chatProvider.ProviderInfo()); + return infosList; + } + + //public api + IList InitProviderInfos() + { + lock (ChatLock) + { + var Config = Properties.Settings.Default; + var rawdata = Config.ChatProviderData; + if (rawdata == "NEEDS INITIALIZING") + return new List() { new IRCSetupInfo(), new DiscordSetupInfo() }; + + string plaintext; + try + { + plaintext = Helpers.DecryptData(rawdata, Config.ChatProviderEntropy); + + var lists = new JavaScriptSerializer().Deserialize>>(plaintext); + var output = new List(lists.Count); + var foundirc = 0; + var founddiscord = 0; + foreach (var l in lists) + { + var info = new ChatSetupInfo(l); + if (info.Provider == ChatProvider.Discord) + ++founddiscord; + else if (info.Provider == ChatProvider.IRC) + ++foundirc; + output.Add(info); + } + + if (foundirc != 1 || founddiscord != 1) + throw new Exception(); + + return output; + } + catch + { + Config.ChatProviderData = "NEEDS INITIALIZING"; + } + } + //if we get here we want to retry + return InitProviderInfos(); + } + + //public api + public string SetProviderInfo(ChatSetupInfo info) + { + try + { + lock (ChatLock) + { + foreach (var ChatProvider in ChatProviders) + if (info.Provider == ChatProvider.ProviderInfo().Provider) + return ChatProvider.SetProviderInfo(info); + return "Error: Invalid provider: " + info.Provider.ToString(); + } + } + catch (Exception e) + { + return e.ToString(); + } + } + + //public api + public bool Connected(ChatProvider providerType) + { + foreach (var I in ChatProviders) + if (I.ProviderInfo().Provider == providerType) + return I.Connected(); + return false; + } + + /// + /// Reconnect servers that are enabled and disconnected + /// + void ChatConnectivityCheck() + { + foreach (ChatProvider I in Enum.GetValues(typeof(ChatProvider))) + if(!Connected(I)) + Reconnect(I); + } + + //public api + public string Reconnect(ChatProvider providerType) + { + foreach (var I in ChatProviders) + if (I.ProviderInfo().Provider == providerType) + return I.Reconnect(); + return "Could not find specified provider!"; + } + + /// + /// Broadcast a message to appropriate channels based on the message type + /// + /// The message to send + /// The message type + public void SendMessage(string msg, ChatMessageType mt) + { + lock (ChatLock) + { + foreach (var ChatProvider in ChatProviders) + try + { + ChatProvider.SendMessage(msg, mt); + }catch(Exception e) + { + Service.WriteWarning(String.Format("Chat broadcast failed (Provider: {3}) (Flags: {0}) (Message: {1}): {2}", mt, msg, e.ToString(), ChatProvider.ProviderInfo().Provider), EventID.ChatBroadcastFail); + } + } + } + } +} diff --git a/TGServerService/Compiler.cs b/TGServerService/ServerInstance/Compiler.cs similarity index 82% rename from TGServerService/Compiler.cs rename to TGServerService/ServerInstance/Compiler.cs index 71c78187c4..e3247b3424 100644 --- a/TGServerService/Compiler.cs +++ b/TGServerService/ServerInstance/Compiler.cs @@ -1,633 +1,634 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using TGServiceInterface; - -namespace TGServerService -{ - partial class TGStationServer : ITGCompiler - { - #region Win32 Shit - [DllImport("kernel32.dll", SetLastError = true)] - static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, SymbolicLink dwFlags); - enum SymbolicLink - { - File = 0, - Directory = 1 - } - #endregion - - const string StaticDirs = "Static"; - const string StaticBackupDir = "Static_BACKUP"; - - const string LibMySQLFile = "/libmysql.dll"; - - const string GameDir = "Game"; - const string GameDirA = GameDir + "/A"; - const string GameDirB = GameDir + "/B"; - const string GameDirLive = GameDir + "/Live"; - - const string LiveFile = "/TestLive.lk"; - const string ADirTest = GameDirA + LiveFile; - const string BDirTest = GameDirB + LiveFile; - const string LiveDirTest = GameDirLive + LiveFile; - - const string InterfaceDLLName = "TGServiceInterface.dll"; - - object CompilerLock = new object(); - TGCompilerStatus compilerCurrentStatus; - string lastCompilerError; - - Thread CompilerThread; - bool compilationCancellationRequestation = false; - bool canCancelCompilation = false; - bool silentCompile = false; - - bool UpdateStaged = false; - - //deletes leftovers and checks current status - void InitCompiler() - { - if(File.Exists(LiveDirTest)) - File.Delete(LiveDirTest); - compilerCurrentStatus = IsInitialized(); - } - - //public api - public TGCompilerStatus GetStatus() - { - lock (CompilerLock) - { - return compilerCurrentStatus; - } - } - - //public api - public string CompileError() - { - lock (CompilerLock) - { - var err = lastCompilerError; - lastCompilerError = null; - return err; - } - } - - //kills the compiler if its running - void DisposeCompiler() - { - lock (CompilerLock) - { - if (CompilerThread == null || !CompilerThread.IsAlive) - return; - CompilerThread.Abort(); //this will safely kill dm - InitCompiler(); //also cleanup - } - } - - //translates the win32 api call into an exception if it fails - void CreateSymlink(string link, string target) - { - if (!CreateSymbolicLink(new DirectoryInfo(link).FullName, new DirectoryInfo(target).FullName, File.Exists(target) ? SymbolicLink.File : SymbolicLink.Directory)) - throw new Exception(String.Format("Failed to create symlink from {0} to {1}! Error: {2}", target, link, Marshal.GetLastWin32Error())); - } - - //requires CompilerLock to be locked - bool CompilerIdleNoLock() - { - return compilerCurrentStatus == TGCompilerStatus.Uninitialized || compilerCurrentStatus == TGCompilerStatus.Initialized; - } - - //public api - public bool Initialize() - { - lock (CompilerLock) - { - if (!CompilerIdleNoLock()) - return false; - lastCompilerError = null; - compilerCurrentStatus = TGCompilerStatus.Initializing; - CompilerThread = new Thread(new ThreadStart(InitializeImpl)); - CompilerThread.Start(); - return true; - } - } - - //what is says on the tin - TGCompilerStatus IsInitialized() - { - if (File.Exists(GameDirLive + LibMySQLFile)) //its a good tell, jim - return TGCompilerStatus.Initialized; - return TGCompilerStatus.Uninitialized; - } - - void CleanGameFolderList(string GameDir, IList theList) - { - foreach (var I in theList) - { - var the_path = Path.Combine(GameDir, I); - if (Directory.Exists(the_path)) - Directory.Delete(the_path); - } - } - - //we need to remove symlinks before we can recursively delete - void CleanGameFolder() - { - if (Directory.Exists(Path.Combine(GameDirA, InterfaceDLLName))) - Directory.Delete(Path.Combine(GameDirA, InterfaceDLLName)); - - if (Directory.Exists(Path.Combine(GameDirB, InterfaceDLLName))) - Directory.Delete(Path.Combine(GameDirB, InterfaceDLLName)); - - if (Directory.Exists(GameDirLive)) - Directory.Delete(GameDirLive); - } - - //Initializing thread - public void InitializeImpl() - { - try - { - if (DaemonStatus() != TGDreamDaemonStatus.Offline) - { - lock (CompilerLock) - { - lastCompilerError = "Dream daemon must not be running"; - compilerCurrentStatus = IsInitialized(); - return; - } - } - - if (!Exists()) //repo - { - lock (CompilerLock) - { - lastCompilerError = "Repository is not setup!"; - compilerCurrentStatus = IsInitialized(); - return; - } - } - - if (!RepoConfigsMatch()) - { - lock (CompilerLock) - { - lastCompilerError = "Repository TGS3.json does not match cached version! Please update the config appropriately!"; - compilerCurrentStatus = IsInitialized(); - return; - } - } - try - { - SendMessage("DM: Setting up symlinks...", ChatMessageType.DeveloperInfo); - CleanGameFolder(); - Program.DeleteDirectory(GameDir); - - Directory.CreateDirectory(GameDirA); - Directory.CreateDirectory(GameDirB); - - var Config = new RepoConfig(false); - - if (Config != null) { - foreach (var I in Config.StaticDirectoryPaths) - CreateSymlink(Path.Combine(GameDirA, I), Path.Combine(StaticDirs, I)); - foreach (var I in Config.DLLPaths) - CreateSymlink(Path.Combine(GameDirA, I), Path.Combine(StaticDirs, I)); - } - - CreateSymlink(Path.Combine(GameDirA, InterfaceDLLName), InterfaceDLLName); - CreateSymlink(Path.Combine(GameDirB, InterfaceDLLName), InterfaceDLLName); - - CreateSymlink(GameDirLive, GameDirA); - - lock (CompilerLock) - { - compilerCurrentStatus = TGCompilerStatus.Compiling; - silentCompile = true; - } - } - catch (ThreadAbortException) - { - return; - } - catch (Exception e) - { - lock (CompilerLock) - { - SendMessage("DM: Setup failed!", ChatMessageType.DeveloperInfo); - lastCompilerError = e.ToString(); - compilerCurrentStatus = TGCompilerStatus.Uninitialized; - return; - } - } - } - catch (ThreadAbortException) - { - return; - } - CompileImpl(); - } - - //Returns the A or B dir in which the game is NOT running - string GetStagingDir() - { - string TheDir; - if (!Directory.Exists(GameDirLive)) - TheDir = GameDirA; - else - { - File.Create(LiveDirTest).Close(); - try - { - if (File.Exists(ADirTest)) - TheDir = GameDirA; - else if (File.Exists(BDirTest)) - TheDir = GameDirB; - else - throw new Exception("Unable to determine current live directory!"); - } - finally - { - File.Delete(LiveDirTest); - } - - - TheDir = InvertDirectory(TheDir); - - } - //So TheDir is what the Live folder is NOT pointing to - //Now we need to check if DD is running that folder and swap it if necessary - - var rsclock = TheDir + "/" + Properties.Settings.Default.ProjectName + ".rsc.lk"; - if (File.Exists(rsclock)) - { - try - { - File.Delete(rsclock); - } - catch //held open by byond - { - //This means there is a staged update waiting to be applied, we have to unstage it before we can work - Directory.Delete(GameDirLive); - CreateSymlink(GameDirLive, TheDir); - return InvertDirectory(TheDir); - } - } - return TheDir; - } - - //I hope you can read this - string InvertDirectory(string gameDirectory) - { - if (gameDirectory == GameDirA) - return GameDirB; - else - return GameDirA; - } - - //Compiler thread - void CompileImpl() - { - try - { - if (GetVersion(TGByondVersion.Installed) == null) - { - lock (CompilerLock) - { - lastCompilerError = "BYOND not installed!"; - compilerCurrentStatus = TGCompilerStatus.Initialized; - return; - } - } - if (!RepoConfigsMatch()) - { - lock (CompilerLock) - { - lastCompilerError = "Repository TGS3.json does not match cached version! Please update the config appropriately!"; - compilerCurrentStatus = IsInitialized(); - return; - } - } - string resurrectee; - bool repobusy_check = false; - if (!Monitor.TryEnter(RepoLock)) - repobusy_check = true; - - if (!repobusy_check) - { - if (RepoBusy) - repobusy_check = true; - else - RepoBusy = true; - Monitor.Exit(RepoLock); - } - - if (repobusy_check) - { - SendMessage("DM: Copy aborted, repo locked!", ChatMessageType.DeveloperInfo); - lock (CompilerLock) - { - lastCompilerError = "The repo could not be locked for copying"; - compilerCurrentStatus = TGCompilerStatus.Initialized; //still fairly valid - return; - } - } - string CurrentSha; - try - { - bool silent; - lock (CompilerLock) - { - silent = silentCompile; - silentCompile = false; - } - - if (!silent) - SendMessage("DM: Compiling...", ChatMessageType.DeveloperInfo); - - resurrectee = GetStagingDir(); - - var Config = new RepoConfig(false); - var deleteExcludeList = new List { InterfaceDLLName }; - deleteExcludeList.AddRange(Config.StaticDirectoryPaths); - deleteExcludeList.AddRange(Config.DLLPaths); - Program.DeleteDirectory(resurrectee, true, deleteExcludeList); - - - Directory.CreateDirectory(resurrectee + "/.git/logs"); - - foreach (var I in Config.StaticDirectoryPaths) - { - var the_path = Path.Combine(resurrectee, I); - if (!Directory.Exists(the_path)) - CreateSymlink(Path.Combine(resurrectee, I), Path.Combine(StaticDirs, I)); - } - foreach (var I in Config.DLLPaths) - { - var the_path = Path.Combine(resurrectee, I); - if (!File.Exists(the_path)) - CreateSymlink(the_path, Path.Combine(StaticDirs, I)); - } - - if (!File.Exists(Path.Combine(resurrectee, InterfaceDLLName))) - CreateSymlink(Path.Combine(resurrectee, InterfaceDLLName), InterfaceDLLName); - - deleteExcludeList.Add(".git"); - Program.CopyDirectory(RepoPath, resurrectee, deleteExcludeList); - CurrentSha = GetHead(false, out string error); - //just the tip - const string GitLogsDir = "/.git/logs"; - Program.CopyDirectory(RepoPath + GitLogsDir, resurrectee + GitLogsDir); - try - { - File.Copy(PRJobFile, resurrectee + Path.DirectorySeparatorChar + PRJobFile); - } - catch { } - } - finally - { - lock (RepoLock) - { - RepoBusy = false; - } - } - - var res = CreateBackup(); - if (res != null) - lock (CompilerLock) - { - lastCompilerError = res; - compilerCurrentStatus = TGCompilerStatus.Initialized; - return; - } - - var dmeName = ProjectName() + ".dme"; - var dmePath = resurrectee + "/" + dmeName; - if (!File.Exists(dmePath)) - { - var errorMsg = String.Format("Could not find {0}!", dmeName); - SendMessage("DM: " + errorMsg, ChatMessageType.DeveloperInfo); - TGServerService.WriteError(errorMsg, TGServerService.EventID.DMCompileCrash); - lock (CompilerLock) - { - lastCompilerError = errorMsg; - compilerCurrentStatus = TGCompilerStatus.Initialized; - return; - } - } - - if (!PrecompileHook()) - { - lastCompilerError = "The precompile hook failed"; - compilerCurrentStatus = TGCompilerStatus.Initialized; //still fairly valid - TGServerService.WriteWarning("Precompile hook failed!", TGServerService.EventID.DMCompileError); - return; - } - - using (var DM = new Process()) //will kill the process if the thread is terminated - { - DM.StartInfo.FileName = ByondDirectory + "/bin/dm.exe"; - DM.StartInfo.Arguments = String.Format("-clean {0}", dmePath); - DM.StartInfo.RedirectStandardOutput = true; - DM.StartInfo.UseShellExecute = false; - var OutputList = new StringBuilder(); - DM.OutputDataReceived += new DataReceivedEventHandler( - delegate (object sender, DataReceivedEventArgs e) - { - OutputList.Append(Environment.NewLine); - OutputList.Append(e.Data); - } - ); - try - { - lock (CompilerLock) - { - if (compilationCancellationRequestation) - return; - canCancelCompilation = true; - } - - DM.Start(); - DM.BeginOutputReadLine(); - while (!DM.HasExited) - DM.WaitForExit(100); - DM.CancelOutputRead(); - - lock (CompilerLock) - { - canCancelCompilation = false; - compilationCancellationRequestation = false; - } - } - catch - { - if (!DM.HasExited) - { - DM.Kill(); - DM.WaitForExit(); - } - throw; - } - finally - { - lock (CompilerLock) - { - canCancelCompilation = false; - } - } - - if (DM.ExitCode == 0) - { - lock (watchdogLock) - { - try - { - //gotta go fast - var online = currentStatus == TGDreamDaemonStatus.Online; - if (online) - Proc.Suspend(); - try - { - if (Directory.Exists(GameDirLive)) - //these two lines should be atomic but this is the best we can do - Directory.Delete(GameDirLive); - CreateSymlink(GameDirLive, resurrectee); - } - finally - { - if (online && !Proc.HasExited) - Proc.Resume(); - } - } - finally - { - if (currentStatus == TGDreamDaemonStatus.Online) - { - try - { - Proc.PriorityClass = ProcessPriorityClass.Normal; - } - catch { } - Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.Normal; - Thread.CurrentThread.Priority = ThreadPriority.Normal; - } - } - } - var staged = DaemonStatus() != TGDreamDaemonStatus.Offline; - if (!PostcompileHook()) - { - lastCompilerError = "The postcompile hook failed"; - compilerCurrentStatus = TGCompilerStatus.Initialized; //still fairly valid - TGServerService.WriteWarning("Postcompile hook failed!", TGServerService.EventID.DMCompileError); - return; - } - UpdateLiveSha(CurrentSha); - var msg = String.Format("Compile complete!{0}", !staged ? "" : " Server will update next round."); - SendMessage("DM: " + msg, ChatMessageType.DeveloperInfo); - TGServerService.WriteInfo(msg, TGServerService.EventID.DMCompileSuccess); - lock (CompilerLock) - { - if (staged) - UpdateStaged = true; - lastCompilerError = null; - compilerCurrentStatus = TGCompilerStatus.Initialized; //still fairly valid - } - } - else - { - SendMessage("DM: Compile failed!", ChatMessageType.DeveloperInfo); //Also happens for warnings - TGServerService.WriteWarning("Compile error: " + OutputList.ToString(), TGServerService.EventID.DMCompileError); - lock (CompilerLock) - { - lastCompilerError = "DM compile failure"; - compilerCurrentStatus = TGCompilerStatus.Initialized; - } - } - } - - } - catch (ThreadAbortException) - { - return; - } - catch (Exception e) - { - SendMessage("DM: Compiler thread crashed!", ChatMessageType.DeveloperInfo); - TGServerService.WriteError("Compile manager errror: " + e.ToString(), TGServerService.EventID.DMCompileCrash); - lock (CompilerLock) - { - lastCompilerError = e.ToString(); - compilerCurrentStatus = TGCompilerStatus.Initialized; //still fairly valid - } - } - finally - { - lock (CompilerLock) - { - canCancelCompilation = false; - if (compilationCancellationRequestation) - { - compilerCurrentStatus = TGCompilerStatus.Initialized; - compilationCancellationRequestation = false; - SendMessage("DM: Compile cancelled!", ChatMessageType.DeveloperInfo); - TGServerService.WriteInfo("Compilation cancelled", TGServerService.EventID.DMCompileCancel); - } - } - } - } - //kicks off the compiler thread - //public api - public bool Compile(bool silent = false) - { - lock (CompilerLock) - { - if (compilerCurrentStatus != TGCompilerStatus.Initialized) - return false; - silentCompile = silent; - lastCompilerError = null; - compilerCurrentStatus = TGCompilerStatus.Compiling; - CompilerThread = new Thread(new ThreadStart(CompileImpl)); - CompilerThread.Start(); - } - return true; - } - - //public api - public string ProjectName() - { - lock (CompilerLock) - { - return Properties.Settings.Default.ProjectName; - } - } - - //public api - public void SetProjectName(string projectName) - { - lock (CompilerLock) - { - Properties.Settings.Default.ProjectName = projectName; - } - } - - public string Cancel() - { - lock (CompilerLock) - { - if (compilerCurrentStatus != TGCompilerStatus.Compiling) - return "Invalid state for cancellation!"; - compilationCancellationRequestation = true; - if (canCancelCompilation) - CompilerThread.Abort(); - else - return "Compilation will be cancelled when the repo copy is complete"; - return null; - } - } - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using TGServiceInterface; +using TGServiceInterface.Components; + +namespace TGServerService +{ + partial class ServerInstance : ITGCompiler + { + #region Win32 Shit + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, SymbolicLink dwFlags); + enum SymbolicLink + { + File = 0, + Directory = 1 + } + #endregion + + const string StaticDirs = "Static"; + const string StaticBackupDir = "Static_BACKUP"; + + const string LibMySQLFile = "/libmysql.dll"; + + const string GameDir = "Game"; + const string GameDirA = GameDir + "/A"; + const string GameDirB = GameDir + "/B"; + const string GameDirLive = GameDir + "/Live"; + + const string LiveFile = "/TestLive.lk"; + const string ADirTest = GameDirA + LiveFile; + const string BDirTest = GameDirB + LiveFile; + const string LiveDirTest = GameDirLive + LiveFile; + + const string InterfaceDLLName = "TGServiceInterface.dll"; + + object CompilerLock = new object(); + CompilerStatus compilerCurrentStatus; + string lastCompilerError; + + Thread CompilerThread; + bool compilationCancellationRequestation = false; + bool canCancelCompilation = false; + bool silentCompile = false; + + bool UpdateStaged = false; + + //deletes leftovers and checks current status + void InitCompiler() + { + if(File.Exists(LiveDirTest)) + File.Delete(LiveDirTest); + compilerCurrentStatus = IsInitialized(); + } + + //public api + public CompilerStatus GetStatus() + { + lock (CompilerLock) + { + return compilerCurrentStatus; + } + } + + //public api + public string CompileError() + { + lock (CompilerLock) + { + var err = lastCompilerError; + lastCompilerError = null; + return err; + } + } + + //kills the compiler if its running + void DisposeCompiler() + { + lock (CompilerLock) + { + if (CompilerThread == null || !CompilerThread.IsAlive) + return; + CompilerThread.Abort(); //this will safely kill dm + InitCompiler(); //also cleanup + } + } + + //translates the win32 api call into an exception if it fails + void CreateSymlink(string link, string target) + { + if (!CreateSymbolicLink(new DirectoryInfo(link).FullName, new DirectoryInfo(target).FullName, File.Exists(target) ? SymbolicLink.File : SymbolicLink.Directory)) + throw new Exception(String.Format("Failed to create symlink from {0} to {1}! Error: {2}", target, link, Marshal.GetLastWin32Error())); + } + + //requires CompilerLock to be locked + bool CompilerIdleNoLock() + { + return compilerCurrentStatus == CompilerStatus.Uninitialized || compilerCurrentStatus == CompilerStatus.Initialized; + } + + //public api + public bool Initialize() + { + lock (CompilerLock) + { + if (!CompilerIdleNoLock()) + return false; + lastCompilerError = null; + compilerCurrentStatus = CompilerStatus.Initializing; + CompilerThread = new Thread(new ThreadStart(InitializeImpl)); + CompilerThread.Start(); + return true; + } + } + + //what is says on the tin + CompilerStatus IsInitialized() + { + if (File.Exists(GameDirLive + LibMySQLFile)) //its a good tell, jim + return CompilerStatus.Initialized; + return CompilerStatus.Uninitialized; + } + + void CleanGameFolderList(string GameDir, IList theList) + { + foreach (var I in theList) + { + var the_path = Path.Combine(GameDir, I); + if (Directory.Exists(the_path)) + Directory.Delete(the_path); + } + } + + //we need to remove symlinks before we can recursively delete + void CleanGameFolder() + { + if (Directory.Exists(Path.Combine(GameDirA, InterfaceDLLName))) + Directory.Delete(Path.Combine(GameDirA, InterfaceDLLName)); + + if (Directory.Exists(Path.Combine(GameDirB, InterfaceDLLName))) + Directory.Delete(Path.Combine(GameDirB, InterfaceDLLName)); + + if (Directory.Exists(GameDirLive)) + Directory.Delete(GameDirLive); + } + + //Initializing thread + public void InitializeImpl() + { + try + { + if (DaemonStatus() != DreamDaemonStatus.Offline) + { + lock (CompilerLock) + { + lastCompilerError = "Dream daemon must not be running"; + compilerCurrentStatus = IsInitialized(); + return; + } + } + + if (!Exists()) //repo + { + lock (CompilerLock) + { + lastCompilerError = "Repository is not setup!"; + compilerCurrentStatus = IsInitialized(); + return; + } + } + + if (!RepoConfigsMatch()) + { + lock (CompilerLock) + { + lastCompilerError = "Repository TGS3.json does not match cached version! Please update the config appropriately!"; + compilerCurrentStatus = IsInitialized(); + return; + } + } + try + { + SendMessage("DM: Setting up symlinks...", ChatMessageType.DeveloperInfo); + CleanGameFolder(); + Program.DeleteDirectory(GameDir); + + Directory.CreateDirectory(GameDirA); + Directory.CreateDirectory(GameDirB); + + var Config = new RepoConfig(false); + + if (Config != null) { + foreach (var I in Config.StaticDirectoryPaths) + CreateSymlink(Path.Combine(GameDirA, I), Path.Combine(StaticDirs, I)); + foreach (var I in Config.DLLPaths) + CreateSymlink(Path.Combine(GameDirA, I), Path.Combine(StaticDirs, I)); + } + + CreateSymlink(Path.Combine(GameDirA, InterfaceDLLName), InterfaceDLLName); + CreateSymlink(Path.Combine(GameDirB, InterfaceDLLName), InterfaceDLLName); + + CreateSymlink(GameDirLive, GameDirA); + + lock (CompilerLock) + { + compilerCurrentStatus = CompilerStatus.Compiling; + silentCompile = true; + } + } + catch (ThreadAbortException) + { + return; + } + catch (Exception e) + { + lock (CompilerLock) + { + SendMessage("DM: Setup failed!", ChatMessageType.DeveloperInfo); + lastCompilerError = e.ToString(); + compilerCurrentStatus = CompilerStatus.Uninitialized; + return; + } + } + } + catch (ThreadAbortException) + { + return; + } + CompileImpl(); + } + + //Returns the A or B dir in which the game is NOT running + string GetStagingDir() + { + string TheDir; + if (!Directory.Exists(GameDirLive)) + TheDir = GameDirA; + else + { + File.Create(LiveDirTest).Close(); + try + { + if (File.Exists(ADirTest)) + TheDir = GameDirA; + else if (File.Exists(BDirTest)) + TheDir = GameDirB; + else + throw new Exception("Unable to determine current live directory!"); + } + finally + { + File.Delete(LiveDirTest); + } + + + TheDir = InvertDirectory(TheDir); + + } + //So TheDir is what the Live folder is NOT pointing to + //Now we need to check if DD is running that folder and swap it if necessary + + var rsclock = TheDir + "/" + Properties.Settings.Default.ProjectName + ".rsc.lk"; + if (File.Exists(rsclock)) + { + try + { + File.Delete(rsclock); + } + catch //held open by byond + { + //This means there is a staged update waiting to be applied, we have to unstage it before we can work + Directory.Delete(GameDirLive); + CreateSymlink(GameDirLive, TheDir); + return InvertDirectory(TheDir); + } + } + return TheDir; + } + + //I hope you can read this + string InvertDirectory(string gameDirectory) + { + if (gameDirectory == GameDirA) + return GameDirB; + else + return GameDirA; + } + + //Compiler thread + void CompileImpl() + { + try + { + if (GetVersion(ByondVersion.Installed) == null) + { + lock (CompilerLock) + { + lastCompilerError = "BYOND not installed!"; + compilerCurrentStatus = CompilerStatus.Initialized; + return; + } + } + if (!RepoConfigsMatch()) + { + lock (CompilerLock) + { + lastCompilerError = "Repository TGS3.json does not match cached version! Please update the config appropriately!"; + compilerCurrentStatus = IsInitialized(); + return; + } + } + string resurrectee; + bool repobusy_check = false; + if (!Monitor.TryEnter(RepoLock)) + repobusy_check = true; + + if (!repobusy_check) + { + if (RepoBusy) + repobusy_check = true; + else + RepoBusy = true; + Monitor.Exit(RepoLock); + } + + if (repobusy_check) + { + SendMessage("DM: Copy aborted, repo locked!", ChatMessageType.DeveloperInfo); + lock (CompilerLock) + { + lastCompilerError = "The repo could not be locked for copying"; + compilerCurrentStatus = CompilerStatus.Initialized; //still fairly valid + return; + } + } + string CurrentSha; + try + { + bool silent; + lock (CompilerLock) + { + silent = silentCompile; + silentCompile = false; + } + + if (!silent) + SendMessage("DM: Compiling...", ChatMessageType.DeveloperInfo); + + resurrectee = GetStagingDir(); + + var Config = new RepoConfig(false); + var deleteExcludeList = new List { InterfaceDLLName }; + deleteExcludeList.AddRange(Config.StaticDirectoryPaths); + deleteExcludeList.AddRange(Config.DLLPaths); + Program.DeleteDirectory(resurrectee, true, deleteExcludeList); + + + Directory.CreateDirectory(resurrectee + "/.git/logs"); + + foreach (var I in Config.StaticDirectoryPaths) + { + var the_path = Path.Combine(resurrectee, I); + if (!Directory.Exists(the_path)) + CreateSymlink(Path.Combine(resurrectee, I), Path.Combine(StaticDirs, I)); + } + foreach (var I in Config.DLLPaths) + { + var the_path = Path.Combine(resurrectee, I); + if (!File.Exists(the_path)) + CreateSymlink(the_path, Path.Combine(StaticDirs, I)); + } + + if (!File.Exists(Path.Combine(resurrectee, InterfaceDLLName))) + CreateSymlink(Path.Combine(resurrectee, InterfaceDLLName), InterfaceDLLName); + + deleteExcludeList.Add(".git"); + Program.CopyDirectory(RepoPath, resurrectee, deleteExcludeList); + CurrentSha = GetHead(false, out string error); + //just the tip + const string GitLogsDir = "/.git/logs"; + Program.CopyDirectory(RepoPath + GitLogsDir, resurrectee + GitLogsDir); + try + { + File.Copy(PRJobFile, resurrectee + Path.DirectorySeparatorChar + PRJobFile); + } + catch { } + } + finally + { + lock (RepoLock) + { + RepoBusy = false; + } + } + + var res = CreateBackup(); + if (res != null) + lock (CompilerLock) + { + lastCompilerError = res; + compilerCurrentStatus = CompilerStatus.Initialized; + return; + } + + var dmeName = ProjectName() + ".dme"; + var dmePath = resurrectee + "/" + dmeName; + if (!File.Exists(dmePath)) + { + var errorMsg = String.Format("Could not find {0}!", dmeName); + SendMessage("DM: " + errorMsg, ChatMessageType.DeveloperInfo); + Service.WriteError(errorMsg, EventID.DMCompileCrash); + lock (CompilerLock) + { + lastCompilerError = errorMsg; + compilerCurrentStatus = CompilerStatus.Initialized; + return; + } + } + + if (!PrecompileHook()) + { + lastCompilerError = "The precompile hook failed"; + compilerCurrentStatus = CompilerStatus.Initialized; //still fairly valid + Service.WriteWarning("Precompile hook failed!", EventID.DMCompileError); + return; + } + + using (var DM = new Process()) //will kill the process if the thread is terminated + { + DM.StartInfo.FileName = ByondDirectory + "/bin/dm.exe"; + DM.StartInfo.Arguments = String.Format("-clean {0}", dmePath); + DM.StartInfo.RedirectStandardOutput = true; + DM.StartInfo.UseShellExecute = false; + var OutputList = new StringBuilder(); + DM.OutputDataReceived += new DataReceivedEventHandler( + delegate (object sender, DataReceivedEventArgs e) + { + OutputList.Append(Environment.NewLine); + OutputList.Append(e.Data); + } + ); + try + { + lock (CompilerLock) + { + if (compilationCancellationRequestation) + return; + canCancelCompilation = true; + } + + DM.Start(); + DM.BeginOutputReadLine(); + while (!DM.HasExited) + DM.WaitForExit(100); + DM.CancelOutputRead(); + + lock (CompilerLock) + { + canCancelCompilation = false; + compilationCancellationRequestation = false; + } + } + catch + { + if (!DM.HasExited) + { + DM.Kill(); + DM.WaitForExit(); + } + throw; + } + finally + { + lock (CompilerLock) + { + canCancelCompilation = false; + } + } + + if (DM.ExitCode == 0) + { + lock (watchdogLock) + { + try + { + //gotta go fast + var online = currentStatus == DreamDaemonStatus.Online; + if (online) + Proc.Suspend(); + try + { + if (Directory.Exists(GameDirLive)) + //these two lines should be atomic but this is the best we can do + Directory.Delete(GameDirLive); + CreateSymlink(GameDirLive, resurrectee); + } + finally + { + if (online && !Proc.HasExited) + Proc.Resume(); + } + } + finally + { + if (currentStatus == DreamDaemonStatus.Online) + { + try + { + Proc.PriorityClass = ProcessPriorityClass.Normal; + } + catch { } + Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.Normal; + Thread.CurrentThread.Priority = ThreadPriority.Normal; + } + } + } + var staged = DaemonStatus() != DreamDaemonStatus.Offline; + if (!PostcompileHook()) + { + lastCompilerError = "The postcompile hook failed"; + compilerCurrentStatus = CompilerStatus.Initialized; //still fairly valid + Service.WriteWarning("Postcompile hook failed!", EventID.DMCompileError); + return; + } + UpdateLiveSha(CurrentSha); + var msg = String.Format("Compile complete!{0}", !staged ? "" : " Server will update next round."); + SendMessage("DM: " + msg, ChatMessageType.DeveloperInfo); + Service.WriteInfo(msg, EventID.DMCompileSuccess); + lock (CompilerLock) + { + if (staged) + UpdateStaged = true; + lastCompilerError = null; + compilerCurrentStatus = CompilerStatus.Initialized; //still fairly valid + } + } + else + { + SendMessage("DM: Compile failed!", ChatMessageType.DeveloperInfo); //Also happens for warnings + Service.WriteWarning("Compile error: " + OutputList.ToString(), EventID.DMCompileError); + lock (CompilerLock) + { + lastCompilerError = "DM compile failure"; + compilerCurrentStatus = CompilerStatus.Initialized; + } + } + } + + } + catch (ThreadAbortException) + { + return; + } + catch (Exception e) + { + SendMessage("DM: Compiler thread crashed!", ChatMessageType.DeveloperInfo); + Service.WriteError("Compile manager errror: " + e.ToString(), EventID.DMCompileCrash); + lock (CompilerLock) + { + lastCompilerError = e.ToString(); + compilerCurrentStatus = CompilerStatus.Initialized; //still fairly valid + } + } + finally + { + lock (CompilerLock) + { + canCancelCompilation = false; + if (compilationCancellationRequestation) + { + compilerCurrentStatus = CompilerStatus.Initialized; + compilationCancellationRequestation = false; + SendMessage("DM: Compile cancelled!", ChatMessageType.DeveloperInfo); + Service.WriteInfo("Compilation cancelled", EventID.DMCompileCancel); + } + } + } + } + //kicks off the compiler thread + //public api + public bool Compile(bool silent = false) + { + lock (CompilerLock) + { + if (compilerCurrentStatus != CompilerStatus.Initialized) + return false; + silentCompile = silent; + lastCompilerError = null; + compilerCurrentStatus = CompilerStatus.Compiling; + CompilerThread = new Thread(new ThreadStart(CompileImpl)); + CompilerThread.Start(); + } + return true; + } + + //public api + public string ProjectName() + { + lock (CompilerLock) + { + return Properties.Settings.Default.ProjectName; + } + } + + //public api + public void SetProjectName(string projectName) + { + lock (CompilerLock) + { + Properties.Settings.Default.ProjectName = projectName; + } + } + + public string Cancel() + { + lock (CompilerLock) + { + if (compilerCurrentStatus != CompilerStatus.Compiling) + return "Invalid state for cancellation!"; + compilationCancellationRequestation = true; + if (canCancelCompilation) + CompilerThread.Abort(); + else + return "Compilation will be cancelled when the repo copy is complete"; + return null; + } + } + } +} diff --git a/TGServerService/Config.cs b/TGServerService/ServerInstance/Config.cs similarity index 91% rename from TGServerService/Config.cs rename to TGServerService/ServerInstance/Config.cs index 188a2c838d..b5b6bc0dfb 100644 --- a/TGServerService/Config.cs +++ b/TGServerService/ServerInstance/Config.cs @@ -1,15 +1,13 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.ServiceModel; -using System.Threading; -using TGServiceInterface; +using TGServiceInterface.Components; namespace TGServerService { //knobs and such - partial class TGStationServer : ITGConfig + partial class ServerInstance : ITGConfig { object configLock = new object(); //for atomic reads/writes //public api @@ -78,8 +76,8 @@ namespace TGServerService } var output = File.ReadAllText(path); - TGServerService.CancelImpersonation(); - TGServerService.WriteInfo("Read of " + path, TGServerService.EventID.StaticRead); + Service.CancelImpersonation(); + Service.WriteInfo("Read of " + path, EventID.StaticRead); error = null; unauthorized = false; return output; @@ -130,8 +128,8 @@ namespace TGServerService Directory.CreateDirectory(destdir); File.WriteAllText(path, data); - TGServerService.CancelImpersonation(); - TGServerService.WriteInfo("Write to " + path, TGServerService.EventID.StaticWrite); + Service.CancelImpersonation(); + Service.WriteInfo("Write to " + path, EventID.StaticWrite); unauthorized = false; return null; } @@ -181,8 +179,8 @@ namespace TGServerService File.Delete(path); else if (Directory.Exists(path)) Program.DeleteDirectory(path); - TGServerService.CancelImpersonation(); - TGServerService.WriteInfo("Delete of " + path, TGServerService.EventID.StaticDelete); + Service.CancelImpersonation(); + Service.WriteInfo("Delete of " + path, EventID.StaticDelete); unauthorized = false; return null; } diff --git a/TGServerService/DreamDaemon.cs b/TGServerService/ServerInstance/DreamDaemon.cs similarity index 82% rename from TGServerService/DreamDaemon.cs rename to TGServerService/ServerInstance/DreamDaemon.cs index ff6fac5c66..a65c5f162c 100644 --- a/TGServerService/DreamDaemon.cs +++ b/TGServerService/ServerInstance/DreamDaemon.cs @@ -5,12 +5,13 @@ using System.Reflection; using System.Threading; using System.Timers; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGServerService { //manages the dd window. //It's not possible to actually click it while starting it in CL mode, so in order to change visibility etc. It restarts the process when the round ends - partial class TGStationServer : ITGDreamDaemon + partial class ServerInstance : ITGDreamDaemon { enum ShutdownRequestPhase { @@ -29,14 +30,14 @@ namespace TGServerService object watchdogLock = new object(); Thread DDWatchdog; - TGDreamDaemonStatus currentStatus; + DreamDaemonStatus currentStatus; string CurrentDDLog; ushort currentPort = 0; object restartLock = new object(); bool RestartInProgress = false; - TGDreamDaemonSecurity StartingSecurity; + DreamDaemonSecurity StartingSecurity; ShutdownRequestPhase AwaitingShutdown; @@ -52,7 +53,7 @@ namespace TGServerService Proc = Process.GetProcessById(Properties.Settings.Default.ReattachPID); if (Proc == null) throw new Exception("GetProcessById returned null!"); - TGServerService.WriteInfo("Reattached to running DD process!", TGServerService.EventID.DDReattachSuccess); + Service.WriteInfo("Reattached to running DD process!", EventID.DDReattachSuccess); ThreadPool.QueueUserWorkItem(_ => { Thread.Sleep(5000); @@ -68,13 +69,13 @@ namespace TGServerService GameAPIVersion = new Version(Properties.Settings.Default.ReattachAPIVersion); } catch { } - currentStatus = TGDreamDaemonStatus.Online; + currentStatus = DreamDaemonStatus.Online; DDWatchdog = new Thread(new ThreadStart(Watchdog)); DDWatchdog.Start(); } catch (Exception e) { - TGServerService.WriteError(String.Format("Failed to reattach to DreamDaemon! PID: {0}. Exception: {1}", Properties.Settings.Default.ReattachPID, e.ToString()), TGServerService.EventID.DDReattachFail); + Service.WriteError(String.Format("Failed to reattach to DreamDaemon! PID: {0}. Exception: {1}", Properties.Settings.Default.ReattachPID, e.ToString()), EventID.DDReattachFail); } finally { @@ -102,7 +103,7 @@ namespace TGServerService { var Detach = Properties.Settings.Default.ReattachToDD; bool RenameLog = false; - if (DaemonStatus() == TGDreamDaemonStatus.Online) + if (DaemonStatus() == DreamDaemonStatus.Online) { if (!Detach) { @@ -130,7 +131,7 @@ namespace TGServerService } //public api - public TGDreamDaemonStatus DaemonStatus() + public DreamDaemonStatus DaemonStatus() { lock (watchdogLock) { @@ -149,7 +150,7 @@ namespace TGServerService { lock (watchdogLock) { - if (currentStatus != TGDreamDaemonStatus.Online || AwaitingShutdown != ShutdownRequestPhase.None) + if (currentStatus != DreamDaemonStatus.Online || AwaitingShutdown != ShutdownRequestPhase.None) return; AwaitingShutdown = ShutdownRequestPhase.Pinged; } @@ -205,7 +206,7 @@ namespace TGServerService //public api public string Restart() { - if (DaemonStatus() == TGDreamDaemonStatus.Offline) + if (DaemonStatus() == DreamDaemonStatus.Offline) return Start(); lock(restartLock) { @@ -228,7 +229,7 @@ namespace TGServerService { lock (watchdogLock) { - if (currentStatus != TGDreamDaemonStatus.Online || CurrentDDLog == null) + if (currentStatus != DreamDaemonStatus.Online || CurrentDDLog == null) return; File.AppendAllText(Path.Combine(ResourceDiagnosticsDir, CurrentDDLog), String.Format("[{0}]: {1}\n", DateTime.Now.ToLongTimeString(), message)); } @@ -244,12 +245,12 @@ namespace TGServerService if (!RestartInProgress) { SendMessage("DD: Server started, watchdog active...", ChatMessageType.WatchdogInfo); - TGServerService.WriteInfo("Watchdog started", TGServerService.EventID.DDWatchdogStarted); + Service.WriteInfo("Watchdog started", EventID.DDWatchdogStarted); } else { RestartInProgress = false; - TGServerService.WriteInfo("Watchdog started", TGServerService.EventID.DDWatchdogRestarted); + Service.WriteInfo("Watchdog started", EventID.DDWatchdogRestarted); } } var retries = 0; @@ -290,7 +291,7 @@ namespace TGServerService lock (watchdogLock) { - currentStatus = TGDreamDaemonStatus.HardRebooting; + currentStatus = DreamDaemonStatus.HardRebooting; currentPort = 0; Proc.Close(); @@ -309,7 +310,7 @@ namespace TGServerService retries = 0; var msg = "DD: DreamDaemon crashed! Watchdog rebooting DD..."; SendMessage(msg, ChatMessageType.WatchdogInfo); - TGServerService.WriteWarning(msg, TGServerService.EventID.DDWatchdogRebootingServer); + Service.WriteWarning(msg, EventID.DDWatchdogRebootingServer); } } @@ -343,23 +344,23 @@ namespace TGServerService catch (Exception e) { SendMessage("DD: Watchdog thread crashed!", ChatMessageType.WatchdogInfo); - TGServerService.WriteError("Watch dog thread crashed: " + e.ToString(), TGServerService.EventID.DDWatchdogCrash); + Service.WriteError("Watch dog thread crashed: " + e.ToString(), EventID.DDWatchdogCrash); } finally { lock (watchdogLock) { - currentStatus = TGDreamDaemonStatus.Offline; + currentStatus = DreamDaemonStatus.Offline; currentPort = 0; AwaitingShutdown = ShutdownRequestPhase.None; if (!RestartInProgress) { if(!Properties.Settings.Default.ReattachToDD) SendMessage("DD: Server stopped, watchdog exiting...", ChatMessageType.WatchdogInfo); - TGServerService.WriteInfo("Watch dog exited", TGServerService.EventID.DDWatchdogExit); + Service.WriteInfo("Watch dog exited", EventID.DDWatchdogExit); } else - TGServerService.WriteInfo("Watch dog restarting...", TGServerService.EventID.DDWatchdogRestart); + Service.WriteInfo("Watch dog restarting...", EventID.DDWatchdogRestart); } } } @@ -389,7 +390,7 @@ namespace TGServerService string CanStartImpl() { - if (GetVersion(TGByondVersion.Installed) == null) + if (GetVersion(ByondVersion.Installed) == null) return "Byond is not installed!"; var DMB = GameDirLive + "/" + Properties.Settings.Default.ProjectName + ".dmb"; if (!File.Exists(DMB)) @@ -400,7 +401,7 @@ namespace TGServerService //public api public string Start() { - if (CurrentStatus() == TGByondStatus.Staged) + if (CurrentStatus() == ByondStatus.Staged) { //IMPORTANT: SLEEP FOR A MOMENT OR WONDOWS WON'T RELEASE THE FUCKING BYOND DLL HANDLES!!!! REEEEEEE Thread.Sleep(3000); @@ -408,13 +409,13 @@ namespace TGServerService } lock (watchdogLock) { - if (currentStatus != TGDreamDaemonStatus.Offline) + if (currentStatus != DreamDaemonStatus.Offline) return "Server already running"; var res = CanStartImpl(); if (res != null) return res; currentPort = 0; - currentStatus = TGDreamDaemonStatus.HardRebooting; + currentStatus = DreamDaemonStatus.HardRebooting; } return StartImpl(false); } @@ -422,14 +423,14 @@ namespace TGServerService //translate the configured security level into a byond param string SecurityWord(bool starting = false) { - var level = starting ? StartingSecurity : (TGDreamDaemonSecurity)Properties.Settings.Default.ServerSecurity; + var level = starting ? StartingSecurity : (DreamDaemonSecurity)Properties.Settings.Default.ServerSecurity; switch (level) { - case TGDreamDaemonSecurity.Safe: + case DreamDaemonSecurity.Safe: return "safe"; - case TGDreamDaemonSecurity.Trusted: + case DreamDaemonSecurity.Trusted: return "trusted"; - case TGDreamDaemonSecurity.Ultrasafe: + case DreamDaemonSecurity.Ultrasafe: return "ultrasafe"; default: throw new Exception(String.Format("Bad DreamDaemon security level: {0}", level)); @@ -441,7 +442,7 @@ namespace TGServerService if (File.Exists(InterfaceDLLName) && !overwrite) return; //Copy the interface dll to the static dir - var InterfacePath = Assembly.GetAssembly(typeof(DDInteropCallHolder)).Location; + var InterfacePath = Assembly.GetAssembly(typeof(DreamDaemonBridge)).Location; File.Copy(InterfacePath, InterfaceDLLName, overwrite); } @@ -460,7 +461,7 @@ namespace TGServerService var DMB = GameDirLive + "/" + Config.ProjectName + ".dmb"; GenCommsKey(); - StartingSecurity = (TGDreamDaemonSecurity)Config.ServerSecurity; + StartingSecurity = (DreamDaemonSecurity)Config.ServerSecurity; Proc.StartInfo.Arguments = String.Format("{0} -port {1} {5}-close -verbose -params \"server_service={3}&server_service_version={4}\" -{2} -public", DMB, Config.ServerPort, SecurityWord(), serviceCommsKey, Version(), Config.Webclient ? "-webclient " : ""); UpdateInterfaceDll(true); lock (topicLock) @@ -474,12 +475,12 @@ namespace TGServerService Proc.Kill(); Proc.WaitForExit(); Proc.Close(); - currentStatus = TGDreamDaemonStatus.Offline; + currentStatus = DreamDaemonStatus.Offline; currentPort = 0; return String.Format("Server start is taking more than {0}s! Aborting!", DDHangStartTime); } currentPort = Config.ServerPort; - currentStatus = TGDreamDaemonStatus.Online; + currentStatus = DreamDaemonStatus.Online; if (!watchdog) { DDWatchdog = new Thread(new ThreadStart(Watchdog)); @@ -490,22 +491,22 @@ namespace TGServerService } catch (Exception e) { - currentStatus = TGDreamDaemonStatus.Offline; + currentStatus = DreamDaemonStatus.Offline; return e.ToString(); } } //public api - public TGDreamDaemonSecurity SecurityLevel() + public DreamDaemonSecurity SecurityLevel() { lock (watchdogLock) { - return (TGDreamDaemonSecurity)Properties.Settings.Default.ServerSecurity; + return (DreamDaemonSecurity)Properties.Settings.Default.ServerSecurity; } } //public api - public bool SetSecurityLevel(TGDreamDaemonSecurity level) + public bool SetSecurityLevel(DreamDaemonSecurity level) { var Config = Properties.Settings.Default; var secInt = (int)level; @@ -517,7 +518,7 @@ namespace TGServerService } if (needReboot) RequestRestart(); - return DaemonStatus() != TGDreamDaemonStatus.Online; + return DaemonStatus() != DreamDaemonStatus.Online; } //public api @@ -540,13 +541,13 @@ namespace TGServerService var ds = DaemonStatus(); switch (ds) { - case TGDreamDaemonStatus.Offline: + case DreamDaemonStatus.Offline: res = "OFFLINE"; break; - case TGDreamDaemonStatus.HardRebooting: + case DreamDaemonStatus.HardRebooting: res = "REBOOTING"; break; - case TGDreamDaemonStatus.Online: + case DreamDaemonStatus.Online: res = "ONLINE"; if (includeMetaInfo) { @@ -562,7 +563,7 @@ namespace TGServerService res = "NULL AND ERRORS"; break; } - if (includeMetaInfo && ds != TGDreamDaemonStatus.Online) + if (includeMetaInfo && ds != DreamDaemonStatus.Online) res += String.Format(visSecStr, SecurityWord()); return res; } @@ -585,7 +586,7 @@ namespace TGServerService /// public string WorldAnnounce(string message) { - var res = SendCommand(SCWorldAnnounce + ";message=" + SanitizeTopicString(message)); + var res = SendCommand(SCWorldAnnounce + ";message=" + Program.SanitizeTopicString(message)); if (res == "SUCCESS") return null; return res; diff --git a/TGServerService/Interop.cs b/TGServerService/ServerInstance/Interop.cs similarity index 83% rename from TGServerService/Interop.cs rename to TGServerService/ServerInstance/Interop.cs index 42ea994f50..a72c7a2ca4 100644 --- a/TGServerService/Interop.cs +++ b/TGServerService/ServerInstance/Interop.cs @@ -1,253 +1,250 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Web.Script.Serialization; -using System.Web.Security; -using TGServiceInterface; - -namespace TGServerService -{ - //handles talking between the world and us - partial class TGStationServer : ITGInterop - { - - object topicLock = new object(); - const int CommsKeyLen = 64; - string serviceCommsKey; //regenerated every DD restart - - //range of supported api versions - readonly Version MinAPIVersion = new Version("3.1.0.0"); - readonly Version MaxAPIVersion = new Version("3.1.0.99"); - Version GameAPIVersion; - - //See code/modules/server_tools/server_tools.dm for command switch - const string SCHardReboot = "hard_reboot"; //requests that dreamdaemon restarts when the round ends - const string SCGracefulShutdown = "graceful_shutdown"; //requests that dreamdaemon stops when the round ends - const string SCWorldAnnounce = "world_announce"; //sends param 'message' to the world - const string SCListCustomCommands = "list_custom_commands"; //Get a list of commands supported by the server - const string SCAPICompat = "api_compat"; //Tells the server we understand each other - const string SCPlayerCount = "client_count"; //Gets the number of connected clients - - const string SRKillProcess = "killme"; - const string SRIRCBroadcast = "irc"; - const string SRIRCAdminChannelMessage = "send2irc"; - const string SRWorldReboot = "worldreboot"; - const string SRAPIVersion = "api_ver"; - - const string CCPHelpText = "help_text"; - const string CCPAdminOnly = "admin_only"; - const string CCPRequiredParameters = "required_parameters"; - - List ServerChatCommands; - - void LoadServerChatCommands() - { - if (DaemonStatus() != TGDreamDaemonStatus.Online) - return; - var json = SendCommand(SCListCustomCommands); - if (String.IsNullOrWhiteSpace(json)) - return; - List tmp = new List(); - try - { - foreach(var I in new JavaScriptSerializer().Deserialize>>(json)) - tmp.Add(new ServerChatCommand(I.Key, (string)I.Value[CCPHelpText], ((int)I.Value[CCPAdminOnly]) == 1, (int)I.Value[CCPRequiredParameters])); - ServerChatCommands = tmp; - } - catch { } - } - - //raw command string sent here via world.ExportService - void HandleCommand(string cmd) - { - var splits = new List(cmd.Split(' ')); - cmd = splits[0]; - splits.RemoveAt(0); - - bool APIValid; - lock (topicLock) - { - APIValid = CheckAPIVersionConstraints(); - } - - if (!APIValid && cmd != SRAPIVersion) - return; //SPEAK THE LANGUAGE!!! - - switch (cmd) - { - case SRIRCBroadcast: - SendMessage("GAME: " + String.Join(" ", splits), ChatMessageType.GameInfo); - break; - case SRKillProcess: - KillMe(); - break; - case SRIRCAdminChannelMessage: - SendMessage("RELAY: " + String.Join(" ", splits), ChatMessageType.AdminInfo); - break; - case SRWorldReboot: - TGServerService.WriteInfo("World Rebooted", TGServerService.EventID.WorldReboot); - WriteCurrentDDLog("World rebooted"); - ServerChatCommands = null; - ChatConnectivityCheck(); - lock (CompilerLock) - { - if (UpdateStaged) - { - UpdateStaged = false; - lock (topicLock) - { - GameAPIVersion = null; //needs updating - } - TGServerService.WriteInfo("Staged update applied", TGServerService.EventID.ServerUpdateApplied); - } - } - break; - case SRAPIVersion: - lock (topicLock) - { - try - { - GameAPIVersion = new Version(splits[0]); - if (!CheckAPIVersionConstraints()) - throw new Exception(); - } - catch - { - TGServerService.WriteWarning(String.Format("API version of the game ({0}) is incompatible with the current supported API versions (Min: {1}. Max: {2}). Interop disabled.", splits.Count > 1 ? splits[1] : "NULL", MinAPIVersion, MaxAPIVersion), TGServerService.EventID.APIVersionMismatch); - GameAPIVersion = null; - break; - } - } - //This needs to be done asyncronously otherwise DD won't be able to process it, because it's waiting for THIS THREAD to return - ThreadPool.QueueUserWorkItem(_ => SendCommand(SCAPICompat)); - break; - } - } - - public string SendCommand(string cmd) - { - lock (watchdogLock) - { - if (currentStatus != TGDreamDaemonStatus.Online) - return "Error: Server Offline!"; - return SendTopic(String.Format("serviceCommsKey={0};command={1}", serviceCommsKey, cmd), currentPort); - } - } - - public int PlayerCount() - { - try - { - return Convert.ToInt32(SendCommand(SCPlayerCount)); - } - catch - { - return -1; - } - } - - //requires topiclock - bool CheckAPIVersionConstraints() - { - return !(GameAPIVersion == null || GameAPIVersion < MinAPIVersion || GameAPIVersion > MaxAPIVersion); - } - - public static string SanitizeTopicString(string input) - { - return input.Replace("%", "%25").Replace("=", "%3d").Replace(";", "%3b").Replace("&", "%26").Replace("+", "%2b"); - } - - //Fuckery to diddle byond with the right packet to accept our girth - string SendTopic(string topicdata, ushort port) - { - //santize the escape characters in accordance with http://www.byond.com/docs/ref/info.html#/proc/params2list - lock (topicLock) { - if (!CheckAPIVersionConstraints()) - return "Incompatible API!"; - using (var topicSender = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { SendTimeout = 5000, ReceiveTimeout = 5000 }) - { - try - { - topicSender.Connect(IPAddress.Loopback, port); - - StringBuilder stringPacket = new StringBuilder(); - stringPacket.Append((char)'\x00', 8); - stringPacket.Append('?' + topicdata); - stringPacket.Append((char)'\x00'); - string fullString = stringPacket.ToString(); - var packet = Encoding.ASCII.GetBytes(fullString); - packet[1] = 0x83; - var FinalLength = packet.Length - 4; - if (FinalLength > UInt16.MaxValue) - return "Error: Topic too long"; - - var lengthBytes = BitConverter.GetBytes((ushort)FinalLength); - - packet[2] = lengthBytes[1]; //fucking endianess - packet[3] = lengthBytes[0]; - - topicSender.Send(packet); - - string returnedString = "NULL"; - try - { - var returnedData = new byte[UInt16.MaxValue]; - topicSender.Receive(returnedData); - var raw_string = Encoding.ASCII.GetString(returnedData).TrimEnd(new char[] { (char)0 }).Trim(); - if (raw_string.Length > 6) - returnedString = raw_string.Substring(5, raw_string.Length - 5).Trim(); - } - catch - { - returnedString = "Topic recieve error!"; - } - finally - { - topicSender.Shutdown(SocketShutdown.Both); - } - - return returnedString; - } - catch - { - return "Topic delivery failed!"; - } - } - } - } - - //Every time we make a new DD process we generate a new comms key for security - //It's in world.params['server_service'] - void GenCommsKey() - { - var charsToRemove = new string[] { "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "-", "+", "=", "[", "{", "]", "}", ";", ":", "<", ">", "|", ".", "/", "?" }; - serviceCommsKey = String.Empty; - do { - var tmp = Membership.GeneratePassword(CommsKeyLen, 0); - foreach (var c in charsToRemove) - tmp = tmp.Replace(c, String.Empty); - serviceCommsKey += tmp; - } while (serviceCommsKey.Length < CommsKeyLen); - serviceCommsKey = serviceCommsKey.Substring(0, CommsKeyLen); - TGServerService.WriteInfo("Service Comms Key set to: " + serviceCommsKey, TGServerService.EventID.CommsKeySet); - } - - /// - public bool InteropMessage(string command) - { - try - { - HandleCommand(command); - return true; - } - catch(Exception e) - { - TGServerService.WriteWarning(String.Format("Handle command for \"{0}\" failed: {1}", command, e.ToString()), TGServerService.EventID.InteropCallException); - return false; - } - } - } -} +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Web.Script.Serialization; +using System.Web.Security; +using TGServerService.ChatCommands; +using TGServiceInterface; +using TGServiceInterface.Components; + +namespace TGServerService +{ + //handles talking between the world and us + partial class ServerInstance : ITGInterop + { + + object topicLock = new object(); + const int CommsKeyLen = 64; + string serviceCommsKey; //regenerated every DD restart + + //range of supported api versions + readonly Version MinAPIVersion = new Version("3.1.0.0"); + readonly Version MaxAPIVersion = new Version("3.1.0.99"); + Version GameAPIVersion; + + //See code/modules/server_tools/server_tools.dm for command switch + const string SCHardReboot = "hard_reboot"; //requests that dreamdaemon restarts when the round ends + const string SCGracefulShutdown = "graceful_shutdown"; //requests that dreamdaemon stops when the round ends + const string SCWorldAnnounce = "world_announce"; //sends param 'message' to the world + const string SCListCustomCommands = "list_custom_commands"; //Get a list of commands supported by the server + const string SCAPICompat = "api_compat"; //Tells the server we understand each other + const string SCPlayerCount = "client_count"; //Gets the number of connected clients + + const string SRKillProcess = "killme"; + const string SRIRCBroadcast = "irc"; + const string SRIRCAdminChannelMessage = "send2irc"; + const string SRWorldReboot = "worldreboot"; + const string SRAPIVersion = "api_ver"; + + const string CCPHelpText = "help_text"; + const string CCPAdminOnly = "admin_only"; + const string CCPRequiredParameters = "required_parameters"; + + List ServerChatCommands; + + void LoadServerChatCommands() + { + if (DaemonStatus() != DreamDaemonStatus.Online) + return; + var json = SendCommand(SCListCustomCommands); + if (String.IsNullOrWhiteSpace(json)) + return; + List tmp = new List(); + try + { + foreach(var I in new JavaScriptSerializer().Deserialize>>(json)) + tmp.Add(new ServerChatCommand(I.Key, (string)I.Value[CCPHelpText], ((int)I.Value[CCPAdminOnly]) == 1, (int)I.Value[CCPRequiredParameters])); + ServerChatCommands = tmp; + } + catch { } + } + + //raw command string sent here via world.ExportService + void HandleCommand(string cmd) + { + var splits = new List(cmd.Split(' ')); + cmd = splits[0]; + splits.RemoveAt(0); + + bool APIValid; + lock (topicLock) + { + APIValid = CheckAPIVersionConstraints(); + } + + if (!APIValid && cmd != SRAPIVersion) + return; //SPEAK THE LANGUAGE!!! + + switch (cmd) + { + case SRIRCBroadcast: + SendMessage("GAME: " + String.Join(" ", splits), ChatMessageType.GameInfo); + break; + case SRKillProcess: + KillMe(); + break; + case SRIRCAdminChannelMessage: + SendMessage("RELAY: " + String.Join(" ", splits), ChatMessageType.AdminInfo); + break; + case SRWorldReboot: + Service.WriteInfo("World Rebooted", EventID.WorldReboot); + WriteCurrentDDLog("World rebooted"); + ServerChatCommands = null; + ChatConnectivityCheck(); + lock (CompilerLock) + { + if (UpdateStaged) + { + UpdateStaged = false; + lock (topicLock) + { + GameAPIVersion = null; //needs updating + } + Service.WriteInfo("Staged update applied", EventID.ServerUpdateApplied); + } + } + break; + case SRAPIVersion: + lock (topicLock) + { + try + { + GameAPIVersion = new Version(splits[0]); + if (!CheckAPIVersionConstraints()) + throw new Exception(); + } + catch + { + Service.WriteWarning(String.Format("API version of the game ({0}) is incompatible with the current supported API versions (Min: {1}. Max: {2}). Interop disabled.", splits.Count > 1 ? splits[1] : "NULL", MinAPIVersion, MaxAPIVersion), EventID.APIVersionMismatch); + GameAPIVersion = null; + break; + } + } + //This needs to be done asyncronously otherwise DD won't be able to process it, because it's waiting for THIS THREAD to return + ThreadPool.QueueUserWorkItem(_ => SendCommand(SCAPICompat)); + break; + } + } + + public string SendCommand(string cmd) + { + lock (watchdogLock) + { + if (currentStatus != DreamDaemonStatus.Online) + return "Error: Server Offline!"; + return SendTopic(String.Format("serviceCommsKey={0};command={1}", serviceCommsKey, cmd), currentPort); + } + } + + public int PlayerCount() + { + try + { + return Convert.ToInt32(SendCommand(SCPlayerCount)); + } + catch + { + return -1; + } + } + + //requires topiclock + bool CheckAPIVersionConstraints() + { + return !(GameAPIVersion == null || GameAPIVersion < MinAPIVersion || GameAPIVersion > MaxAPIVersion); + } + + //Fuckery to diddle byond with the right packet to accept our girth + string SendTopic(string topicdata, ushort port) + { + //santize the escape characters in accordance with http://www.byond.com/docs/ref/info.html#/proc/params2list + lock (topicLock) { + if (!CheckAPIVersionConstraints()) + return "Incompatible API!"; + using (var topicSender = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { SendTimeout = 5000, ReceiveTimeout = 5000 }) + { + try + { + topicSender.Connect(IPAddress.Loopback, port); + + StringBuilder stringPacket = new StringBuilder(); + stringPacket.Append((char)'\x00', 8); + stringPacket.Append('?' + topicdata); + stringPacket.Append((char)'\x00'); + string fullString = stringPacket.ToString(); + var packet = Encoding.ASCII.GetBytes(fullString); + packet[1] = 0x83; + var FinalLength = packet.Length - 4; + if (FinalLength > UInt16.MaxValue) + return "Error: Topic too long"; + + var lengthBytes = BitConverter.GetBytes((ushort)FinalLength); + + packet[2] = lengthBytes[1]; //fucking endianess + packet[3] = lengthBytes[0]; + + topicSender.Send(packet); + + string returnedString = "NULL"; + try + { + var returnedData = new byte[UInt16.MaxValue]; + topicSender.Receive(returnedData); + var raw_string = Encoding.ASCII.GetString(returnedData).TrimEnd(new char[] { (char)0 }).Trim(); + if (raw_string.Length > 6) + returnedString = raw_string.Substring(5, raw_string.Length - 5).Trim(); + } + catch + { + returnedString = "Topic recieve error!"; + } + finally + { + topicSender.Shutdown(SocketShutdown.Both); + } + + return returnedString; + } + catch + { + return "Topic delivery failed!"; + } + } + } + } + + //Every time we make a new DD process we generate a new comms key for security + //It's in world.params['server_service'] + void GenCommsKey() + { + var charsToRemove = new string[] { "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "-", "+", "=", "[", "{", "]", "}", ";", ":", "<", ">", "|", ".", "/", "?" }; + serviceCommsKey = String.Empty; + do { + var tmp = Membership.GeneratePassword(CommsKeyLen, 0); + foreach (var c in charsToRemove) + tmp = tmp.Replace(c, String.Empty); + serviceCommsKey += tmp; + } while (serviceCommsKey.Length < CommsKeyLen); + serviceCommsKey = serviceCommsKey.Substring(0, CommsKeyLen); + Service.WriteInfo("Service Comms Key set to: " + serviceCommsKey, EventID.CommsKeySet); + } + + /// + public bool InteropMessage(string command) + { + try + { + HandleCommand(command); + return true; + } + catch(Exception e) + { + Service.WriteWarning(String.Format("Handle command for \"{0}\" failed: {1}", command, e.ToString()), EventID.InteropCallException); + return false; + } + } + } +} diff --git a/TGServerService/PreactionHandler.cs b/TGServerService/ServerInstance/PreactionHandler.cs similarity index 88% rename from TGServerService/PreactionHandler.cs rename to TGServerService/ServerInstance/PreactionHandler.cs index 802a3da52a..cf593e6d9c 100644 --- a/TGServerService/PreactionHandler.cs +++ b/TGServerService/ServerInstance/PreactionHandler.cs @@ -5,7 +5,7 @@ using System.IO; namespace TGServerService { // Some useful functions for triggering pre action events - partial class TGStationServer + partial class ServerInstance { const string EventFolder = "EventHandlers/"; @@ -44,9 +44,9 @@ namespace TGServerService var stdout = process.StandardOutput.ReadToEnd(); var stderr = process.StandardError.ReadToEnd(); - TGServerService.WriteInfo( + Service.WriteInfo( String.Format("Preaction Event: {0} @ {1} ran. Stdout:\n{2}\nStderr:\n{3}", eventName, GetPath(eventName), stdout, stderr), - process.ExitCode == 0 ? TGServerService.EventID.PreactionEvent : TGServerService.EventID.PreactionFail + process.ExitCode == 0 ? EventID.PreactionEvent : EventID.PreactionFail ); return process.ExitCode == 0 ? true : false; diff --git a/TGServerService/Repository.cs b/TGServerService/ServerInstance/Repository.cs similarity index 86% rename from TGServerService/Repository.cs rename to TGServerService/ServerInstance/Repository.cs index 337b00f089..93829c13e2 100644 --- a/TGServerService/Repository.cs +++ b/TGServerService/ServerInstance/Repository.cs @@ -1,1261 +1,1262 @@ -using LibGit2Sharp; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading; -using System.Web.Script.Serialization; -using TGServiceInterface; - -namespace TGServerService -{ - partial class TGStationServer : ITGRepository, IDisposable - { - const string RepoPath = "Repository"; - const string RepoTGS3SettingsPath = RepoPath + "/TGS3.json"; - const string CachedTGS3SettingsPath = "TGS3.json"; - const string RepoErrorUpToDate = "Already up to date!"; - const string SSHPushRemote = "ssh_push_target"; - const string RepoKeyDir = "RepoKey/"; - const string PrivateKeyPath = RepoKeyDir + "private_key.txt"; - const string PublicKeyPath = RepoKeyDir + "public_key.txt"; - const string PRJobFile = "prtestjob.json"; - const string LiveTrackingBranch = "___TGSLiveCommitTrackingBranch"; - const string CommitMessage = "Automatic changelog compile, [ci skip]"; - - object RepoLock = new object(); - bool RepoBusy = false; - bool Cloning = false; - - Repository Repo; - int currentProgress = -1; - - System.Timers.Timer autoUpdateTimer = new System.Timers.Timer() - { - AutoReset = true - }; - - /// - /// Repo specific information about the installation - /// Requires RepoLock and !RepoBusy to be instantiated - /// - class RepoConfig : IEquatable - { - public readonly bool ChangelogSupport; - public readonly string PathToChangelogPy; - public readonly string ChangelogPyArguments; - public readonly IList PipDependancies = new List(); - public readonly IList PathsToStage = new List(); - public readonly IList StaticDirectoryPaths = new List(); - public readonly IList DLLPaths = new List(); - - public RepoConfig(bool FromRepository) - { - var path = FromRepository ? RepoTGS3SettingsPath : CachedTGS3SettingsPath; - if (!File.Exists(path)) - return; - var rawdata = File.ReadAllText(path); - var Deserializer = new JavaScriptSerializer(); - var json = Deserializer.Deserialize>(rawdata); - try - { - var details = (IDictionary)json["changelog"]; - PathToChangelogPy = (string)details["script"]; - ChangelogPyArguments = (string)details["arguments"]; - ChangelogSupport = true; - try - { - PipDependancies = LoadArray(details["pip_dependancies"]); - } - catch { } - } - catch { - ChangelogSupport = false; - } - try - { - PathsToStage = LoadArray(json["synchronize_paths"]); - } - catch { } - try - { - StaticDirectoryPaths = LoadArray(json["static_directories"]); - } - catch { } - try - { - DLLPaths = LoadArray(json["dlls"]); - } - catch { } - } - private static IList LoadArray(object o) - { - var array = (object[])o; - var res = new List(); - foreach (var I in array) - res.Add((string)I); - return res; - } - - public override bool Equals(object obj) - { - return Equals(obj as RepoConfig); - } - - private static bool ListEquals(IList A, IList B) - { - return A.All(B.Contains) && A.Count == B.Count; - } - - public bool Equals(RepoConfig other) - { - return ChangelogSupport == other.ChangelogSupport - && PathToChangelogPy == other.PathToChangelogPy - && ChangelogPyArguments == other.ChangelogPyArguments - && ListEquals(PipDependancies, other.PipDependancies) - && ListEquals(PathsToStage, other.PathsToStage) - && ListEquals(StaticDirectoryPaths, other.StaticDirectoryPaths) - && ListEquals(DLLPaths, other.DLLPaths); - } - - public override int GetHashCode() - { - var hashCode = 1890628544; - hashCode = hashCode * -1521134295 + ChangelogSupport.GetHashCode(); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(PathToChangelogPy); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ChangelogPyArguments); - hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(PipDependancies); - hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(PathsToStage); - hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(StaticDirectoryPaths); - hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(DLLPaths); - return hashCode; - } - - public static bool operator ==(RepoConfig config1, RepoConfig config2) - { - return EqualityComparer.Default.Equals(config1, config2); - } - - public static bool operator !=(RepoConfig config1, RepoConfig config2) - { - return !(config1 == config2); - } - } - - void InitRepo() - { - Directory.CreateDirectory(RepoKeyDir); - if(Exists()) - UpdateInterfaceDll(false); - if(LoadRepo() == null) - DisableGarbageCollectionNoLock(); - //start the autoupdate timer - autoUpdateTimer.Elapsed += AutoUpdateTimer_Elapsed; - SetAutoUpdateInterval(Properties.Settings.Default.AutoUpdateInterval); - } - - bool RepoConfigsMatch() - { - //this should never be called while the repo is busy - RepoConfig I = null; - lock (RepoLock) - { - if (!RepoBusy && LoadRepo() == null) - I = new RepoConfig(true); - } - if (I == null) - throw new Exception("Unable to load TGS3.json from repo!"); - var J = new RepoConfig(false); - return I == J; - } - - //public api - public bool OperationInProgress() - { - lock (RepoLock) - { - return RepoBusy; - } - } - - //public api - public int CheckoutProgress() - { - return currentProgress; - } - - //Sets up the repo object - string LoadRepo() - { - if (Repo != null) - return null; - if (!Repository.IsValid(RepoPath)) - return "Repository does not exist"; - try - { - Repo = new Repository(RepoPath); - } - catch (Exception e) - { - return e.ToString(); - } - return null; - } - - //Cleans up the repo object - void DisposeRepo() - { - if (Repo != null) - { - Repo.Dispose(); - Repo = null; - } - } - - //public api - public bool Exists() - { - lock (RepoLock) - { - return !Cloning && Repository.IsValid(RepoPath); - } - } - - //Updates the currentProgress var - //no locks required because who gives a shit, it's a fucking 32-bit integer - bool HandleTransferProgress(TransferProgress progress) - { - currentProgress = (int)(((float)progress.ReceivedObjects / progress.TotalObjects) * 100) / 2; - currentProgress += (int)(((float)progress.IndexedObjects / progress.TotalObjects) * 100) / 2; - return true; - } - - //see above - void HandleCheckoutProgress(string path, int completedSteps, int totalSteps) - { - currentProgress = (int)(((float)completedSteps / totalSteps) * 100); - } - - //For the thread parameter - private class TwoStrings - { - public string a, b; - } - - //This is the thread that resets za warldo - //clones, checksout, sets up static dir - void Clone(object twostrings) - { - //busy flag set by caller - var ts = (TwoStrings)twostrings; - var RepoURL = ts.a; - var BranchName = ts.b; - try - { - SendMessage(String.Format("REPO: {2} started: Cloning {0} branch of {1} ...", BranchName, RepoURL, Repository.IsValid(RepoPath) ? "Full reset" : "Setup"), ChatMessageType.DeveloperInfo); - try - { - DisposeRepo(); - Program.DeleteDirectory(RepoPath); - DeletePRList(); - lock (configLock) - { - BackupAndDeleteStaticDirectory(); - } - - var Opts = new CloneOptions() - { - BranchName = BranchName, - RecurseSubmodules = true, - OnTransferProgress = HandleTransferProgress, - OnCheckoutProgress = HandleCheckoutProgress, - CredentialsProvider = GenerateGitCredentials, - }; - - Repository.Clone(RepoURL, RepoPath, Opts); - currentProgress = -1; - LoadRepo(); - - DisableGarbageCollectionNoLock(); - - //create an ssh remote for pushing - Repo.Network.Remotes.Add(SSHPushRemote, RepoURL.Replace("git://", "ssh://").Replace("https://", "ssh://")); - - InitialConfigureRepository(); - - SendMessage("REPO: Clone complete!", ChatMessageType.DeveloperInfo); - TGServerService.WriteInfo("Repository {0}:{1} successfully cloned", TGServerService.EventID.RepoClone); - } - finally - { - currentProgress = -1; - } - } - catch (Exception e) - - { - SendMessage("REPO: Setup failed!", ChatMessageType.DeveloperInfo); - TGServerService.WriteWarning(String.Format("Failed to clone {2}:{0}: {1}", BranchName, e.ToString(), RepoURL), TGServerService.EventID.RepoCloneFail); - } - finally - { - lock (RepoLock) - { - RepoBusy = false; - Cloning = false; - } - } - } - - void DisableGarbageCollectionNoLock() - { - Repo.Config.Set("gc.auto", false); - } - - void BackupAndDeleteStaticDirectory() - { - if (Directory.Exists(StaticDirs)) - { - int count = 1; - - string path = Path.GetDirectoryName(StaticBackupDir); - string newFullPath = StaticBackupDir; - - while (File.Exists(newFullPath) || Directory.Exists(newFullPath)) - { - string tempDirName = string.Format("{0}({1})", StaticBackupDir, count++); - newFullPath = Path.Combine(path, tempDirName); - } - - Program.CopyDirectory(StaticDirs, newFullPath); - } - Program.DeleteDirectory(StaticDirs); - } - - public string UpdateTGS3Json() - { - try - { - if (File.Exists(RepoTGS3SettingsPath)) - File.Copy(RepoTGS3SettingsPath, CachedTGS3SettingsPath, true); - else if (File.Exists(CachedTGS3SettingsPath)) - File.Delete(CachedTGS3SettingsPath); - } - catch(Exception e) - { - return e.ToString(); - } - return null; - } - - void InitialConfigureRepository() - { - Directory.CreateDirectory(StaticDirs); - UpdateInterfaceDll(false); - UpdateTGS3Json(); - var Config = new RepoConfig(false); //RepoBusy is set if we're here - foreach(var I in Config.StaticDirectoryPaths) - { - try - { - var source = Path.Combine(RepoPath, I); - var dest = Path.Combine(StaticDirs, I); - if (Directory.Exists(source)) - Program.CopyDirectory(source, dest); - else - Directory.CreateDirectory(dest); - } - catch - { - TGServerService.WriteWarning("Could not setup static directory: " + I, TGServerService.EventID.RepoConfigurationFail); - } - } - foreach(var I in Config.DLLPaths) - { - try - { - var source = Path.Combine(RepoPath, I); - if (!File.Exists(source)) - { - TGServerService.WriteWarning("Could not find DLL: " + I, TGServerService.EventID.RepoConfigurationFail); - continue; - } - var dest = Path.Combine(StaticDirs, I); - Program.CopyFileForceDirectories(source, dest, false); - } - catch - { - TGServerService.WriteWarning("Could not setup static DLL: " + I, TGServerService.EventID.RepoConfigurationFail); - } - } - } - - //kicks off the cloning thread - //public api - public string Setup(string RepoURL, string BranchName) - { - lock (RepoLock) - { - if (RepoBusy) - return "Repo is busy!"; - lock (CompilerLock) - { - if (!CompilerIdleNoLock()) - return "Compiler is running!"; - } - if (DaemonStatus() != TGDreamDaemonStatus.Offline) - return "DreamDaemon is running!"; - if (RepoURL.Contains("ssh://") && !SSHAuth()) - return String.Format("SSH url specified but either {0} or {1} does not exist in the server directory!", PrivateKeyPath, PublicKeyPath); - RepoBusy = true; - Cloning = true; - new Thread(new ParameterizedThreadStart(Clone)) - { - IsBackground = true //make sure we don't hold up shutdown - }.Start(new TwoStrings { a = RepoURL, b = BranchName }); - return null; - } - } - - //Gets what HEAD is pointing to - string GetShaOrBranch(out string error, bool branch, bool tracked) - { - lock (RepoLock) - { - var result = LoadRepo(); - if (result != null) - { - error = result; - return null; - } - - try - { - error = null; - if (tracked && Repo.Head.TrackedBranch != null) - return Repo.Head.TrackedBranch.Tip.Sha; - return branch ? Repo.Head.FriendlyName : Repo.Head.Tip.Sha; - } - catch (Exception e) - { - error = e.ToString(); - return null; - } - } - } - - //moist shleppy noises - //public api - public string GetHead(bool useTracked, out string error) - { - return GetShaOrBranch(out error, false, useTracked); - } - - //public api - public string GetBranch(out string error) - { - return GetShaOrBranch(out error, true, false); - } - - //public api - public string GetRemote(out string error) - { - try - { - var res = LoadRepo(); - if (res != null) - { - error = res; - return null; - } - error = null; - return Repo.Network.Remotes["origin"].Url; - } - catch (Exception e) - { - error = e.ToString(); - return null; - } - } - - //calls git reset --hard on HEAD - //requires RepoLock - string ResetNoLock(Branch targetBranch) - { - try - { - if (targetBranch != null) - Repo.Reset(ResetMode.Hard, targetBranch.Tip); - else - Repo.Reset(ResetMode.Hard); - return null; - } - catch (Exception e) - { - return e.ToString(); - } - } - - //public api - public string Checkout(string sha) - { - if (sha == LiveTrackingBranch) - return "I'm sorry Dave, I'm afraid I can't do that..."; - lock (RepoLock) - { - var result = LoadRepo(); - if (result != null) - return result; - SendMessage("REPO: Checking out object: " + sha, ChatMessageType.DeveloperInfo); - try - { - if (Repo.Branches[sha] == null) - { - //see if origin has the branch - result = Fetch(); - var trackedBranch = Repo.Branches[String.Format("origin/{0}", sha)]; - if (trackedBranch != null) - { - var newBranch = Repo.CreateBranch(sha, trackedBranch.Tip); - //track it - Repo.Branches.Update(newBranch, b => b.TrackedBranch = trackedBranch.CanonicalName); - } - else if (result != null) - return result; - } - var Opts = new CheckoutOptions() - { - CheckoutModifiers = CheckoutModifiers.Force, - OnCheckoutProgress = HandleCheckoutProgress, - }; - Commands.Checkout(Repo, sha, Opts); - var res = ResetNoLock(null); - UpdateSubmodules(); - SendMessage("REPO: Checkout complete!", ChatMessageType.DeveloperInfo); - TGServerService.WriteInfo("Repo checked out " + sha, TGServerService.EventID.RepoCheckout); - return res; - } - catch (Exception e) - { - SendMessage("REPO: Checkout failed!", ChatMessageType.DeveloperInfo); - TGServerService.WriteWarning(String.Format("Repo checkout of {0} failed: {1}", sha, e.ToString()), TGServerService.EventID.RepoCheckoutFail); - return e.ToString(); - } - } - } - - //Merges a thing into HEAD, not even necessarily a branch - string MergeBranch(string branchname) - { - var mo = new MergeOptions() - { - OnCheckoutProgress = HandleCheckoutProgress - }; - var Result = Repo.Merge(branchname, MakeSig()); - currentProgress = -1; - switch (Result.Status) - { - case MergeStatus.Conflicts: - ResetNoLock(null); - SendMessage("REPO: Merge conflicted, aborted.", ChatMessageType.DeveloperInfo); - return "Merge conflict occurred."; - case MergeStatus.UpToDate: - return RepoErrorUpToDate; - } - return null; - } - - //public api - public string Update(bool reset) - { - return UpdateImpl(reset, true); - } - - string UpdateImpl(bool reset, bool successOnUpToDate) - { - lock (RepoLock) - { - var result = LoadRepo(); - if (result != null) - return result; - try - { - if (Repo.Head == null || !Repo.Head.IsTracking) - return "Cannot update while not on a tracked branch"; - - var res = Fetch(); - if (res != null) - return res; - - var originBranch = Repo.Head.TrackedBranch; - if (!successOnUpToDate && Repo.Head.Tip.Sha == originBranch.Tip.Sha) - return RepoErrorUpToDate; - - SendMessage(String.Format("REPO: Updating origin branch...({0})", reset ? "Hard Reset" : "Merge"), ChatMessageType.DeveloperInfo); - - if (reset) - { - var error = ResetNoLock(Repo.Head.TrackedBranch); - UpdateSubmodules(); - if (error != null) - throw new Exception(error); - DeletePRList(); - TGServerService.WriteInfo("Repo hard updated to " + originBranch.Tip.Sha, TGServerService.EventID.RepoHardUpdate); - return error; - } - res = MergeBranch(originBranch.FriendlyName); - if (res != null) - throw new Exception(res); - UpdateSubmodules(); - TGServerService.WriteInfo("Repo merge updated to " + originBranch.Tip.Sha, TGServerService.EventID.RepoMergeUpdate); - return null; - } - catch (Exception E) - { - SendMessage("REPO: Update failed!", ChatMessageType.DeveloperInfo); - TGServerService.WriteWarning(String.Format("Repo{0} update failed", reset ? " hard" : ""), reset ? TGServerService.EventID.RepoHardUpdateFail : TGServerService.EventID.RepoMergeUpdateFail); - return E.ToString(); - } - } - } - - private void UpdateSubmodules() - { - var suo = new SubmoduleUpdateOptions - { - Init = true - }; - foreach (var I in Repo.Submodules) - try - { - Repo.Submodules.Update(I.Name, suo); - } - catch (Exception e) - { - //workaround for https://github.com/libgit2/libgit2/issues/3820 - //kill off the modules/ folder in .git and try again - try - { - Program.DeleteDirectory(String.Format("{0}/.git/modules/{1}", RepoPath, I.Path)); - } - catch - { - throw e; - } - Repo.Submodules.Update(I.Name, suo); - var msg = String.Format("I had to reclone submodule {0}. If this is happening a lot find a better hack or fix https://github.com/libgit2/libgit2/issues/3820!", I.Name); - SendMessage(String.Format("REPO: {0}", msg), ChatMessageType.DeveloperInfo); - TGServerService.WriteWarning(msg, TGServerService.EventID.SubmoduleReclone); - } - } - - string CreateBackup() - { - try - { - lock (RepoLock) - { - var res = LoadRepo(); - if (res != null) - return res; - - //Make sure we don't already have a backup at this commit - var HEAD = Repo.Head.Tip.Sha; - foreach (var T in Repo.Tags) - if (T.Target.Sha == HEAD) - return null; - - var tagName = "TGS-Compile-Backup-" + DateTime.Now.ToString("yyyy-MM-dd--HH.mm.ss"); - var tag = Repo.ApplyTag(tagName); - - if (tag != null) - { - TGServerService.WriteInfo("Repo backup created at tag: " + tagName + " commit: " + HEAD, TGServerService.EventID.RepoBackupTag); - return null; - } - throw new Exception("Tag creation failed!"); - } - } - catch (Exception e) - { - TGServerService.WriteWarning(String.Format("Failed backup tag creation at commit {0}!", Repo.Head.Tip.Sha), TGServerService.EventID.RepoBackupTagFail); - return e.ToString(); - } - } - - public IDictionary ListBackups(out string error) - { - try - { - lock (RepoLock) - { - error = LoadRepo(); - if (error != null) - return null; - - var res = new Dictionary(); - foreach (var T in Repo.Tags) - if (T.FriendlyName.Contains("TGS")) - res.Add(T.FriendlyName, T.Target.Sha); - return res; - } - } - catch (Exception e) - { - error = e.ToString(); - return null; - } - } - - //public api - public string Reset(bool trackedBranch) - { - lock (RepoLock) - { - var res = LoadRepo() ?? ResetNoLock(trackedBranch ? (Repo.Head.TrackedBranch ?? Repo.Head) : Repo.Head); - if (res == null) - { - SendMessage(String.Format("REPO: Hard reset to {0}branch", trackedBranch ? "tracked " : ""), ChatMessageType.DeveloperInfo); - if (trackedBranch) - DeletePRList(); - TGServerService.WriteInfo(String.Format("Repo branch reset{0}", trackedBranch ? " to tracked branch" : ""), trackedBranch ? TGServerService.EventID.RepoResetTracked : TGServerService.EventID.RepoReset); - return null; - } - TGServerService.WriteWarning(String.Format("Failed to reset{0}: {1}", trackedBranch ? " to tracked branch" : "", res), trackedBranch ? TGServerService.EventID.RepoResetTrackedFail : TGServerService.EventID.RepoResetFail); - return res; - } - } - - //Makes the LibGit2Sharp sig we'll use for committing based on the configured stuff - Signature MakeSig() - { - var Config = Properties.Settings.Default; - return new Signature(new Identity(Config.CommitterName, Config.CommitterEmail), DateTimeOffset.Now); - } - - //I wonder... - void DeletePRList() - { - if (File.Exists(PRJobFile)) - try - { - File.Delete(PRJobFile); - } - catch (Exception e) - { - TGServerService.WriteError("Failed to delete PR list: " + e.ToString(), TGServerService.EventID.RepoPRListError); - } - } - - //json_decode(file2text()) - IDictionary> GetCurrentPRList() - { - if (!File.Exists(PRJobFile)) - return new Dictionary>(); - var rawdata = File.ReadAllText(PRJobFile); - var Deserializer = new JavaScriptSerializer(); - return Deserializer.Deserialize>>(rawdata); - } - - //text2file(json_encode()) - void SetCurrentPRList(IDictionary> list) - { - var Serializer = new JavaScriptSerializer(); - var rawdata = Serializer.Serialize(list); - File.WriteAllText(PRJobFile, rawdata); - } - - //public api - public string MergePullRequest(int PRNumber) - { - return MergePullRequestImpl(PRNumber, false); - } - string MergePullRequestImpl(int PRNumber, bool impliedUpdate) - { - lock (RepoLock) - { - var result = LoadRepo(); - if (result != null) - return result; - SendMessage(String.Format("REPO: {1}erging PR #{0}...", PRNumber, impliedUpdate ? "Test m" : "M"), ChatMessageType.DeveloperInfo); - result = ResetNoLock(null); - if (result != null) - return result; - try - { - //only supported with github - var remoteUrl = Repo.Network.Remotes["origin"].Url; - if (!remoteUrl.Contains("github.com")) - return "Only supported with Github based repositories."; - - - var Refspec = new List(); - var PRBranchName = String.Format("pr-{0}", PRNumber); - var LocalBranchName = String.Format("pull/{0}/headrefs/heads/{1}", PRNumber, PRBranchName); - Refspec.Add(String.Format("pull/{0}/head:{1}", PRNumber, PRBranchName)); - var logMessage = ""; - - var branch = Repo.Branches[LocalBranchName]; - if (branch != null) - //Need to delete the branch first in case of rebase - Repo.Branches.Remove(branch); - - Commands.Fetch(Repo, "origin", Refspec, GenerateFetchOptions(), logMessage); //shitty api has no failure state for this - - currentProgress = -1; - - var Config = Properties.Settings.Default; - - - branch = Repo.Branches[LocalBranchName]; - if (branch == null) - { - SendMessage("REPO: PR could not be fetched. Does it exist?", ChatMessageType.DeveloperInfo); - return String.Format("PR #{0} could not be fetched. Does it exist?", PRNumber); - } - - //so we'll know if this fails - var Result = MergeBranch(LocalBranchName); - - if (Result == null) - try - { - UpdateSubmodules(); - } - catch (Exception e) - { - Result = e.ToString(); - } - - if (Result == null) - { - TGServerService.WriteInfo(String.Format("Merged pull request #{0}", PRNumber), TGServerService.EventID.RepoPRMerge); - try - { - var CurrentPRs = GetCurrentPRList(); - var PRNumberString = PRNumber.ToString(); - CurrentPRs.Remove(PRNumberString); - var newPR = new Dictionary(); - - //do some excellent remote fuckery here to get the api page - var prAPI = remoteUrl; - prAPI = prAPI.Replace("/.git", ""); - prAPI = prAPI.Replace(".git", ""); - prAPI = prAPI.Replace("github.com", "api.github.com/repos"); - prAPI += "/pulls/" + PRNumberString + ".json"; - string json; - using (var wc = new WebClient()) - { - wc.Headers.Add("user-agent", "TGStationServerService"); - json = wc.DownloadString(prAPI); - } - - var Deserializer = new JavaScriptSerializer(); - var dick = Deserializer.DeserializeObject(json) as IDictionary; - var user = dick["user"] as IDictionary; - - newPR.Add("commit", branch.Tip.Sha); - newPR.Add("author", (string)user["login"]); - newPR.Add("title", (string)dick["title"]); - CurrentPRs.Add(PRNumberString, newPR); - SetCurrentPRList(CurrentPRs); - } - catch (Exception e) - { - TGServerService.WriteError("Failed to update PR list", TGServerService.EventID.RepoPRListError); - return "PR Merged, JSON update failed: " + e.ToString(); - } - } - return Result; - } - catch (Exception E) - { - SendMessage("REPO: PR merge failed!", ChatMessageType.DeveloperInfo); - TGServerService.WriteWarning(String.Format("Failed to merge pull request #{0}: {1}", PRNumber, E.ToString()), TGServerService.EventID.RepoPRMergeFail); - return E.ToString(); - } - } - } - - //public api - public IList MergedPullRequests(out string error) - { - lock (RepoLock) - { - var result = LoadRepo(); - if (result != null) - { - error = result; - return null; - } - try - { - var PRRawData = GetCurrentPRList(); - IList output = new List(); - foreach (var I in GetCurrentPRList()) - output.Add(new PullRequestInfo(Convert.ToInt32(I.Key), I.Value["author"], I.Value["title"], I.Value["commit"])); - error = null; - return output; - } - catch (Exception e) - { - error = e.ToString(); - return null; - } - } - } - - //public api - public string GetCommitterName() - { - lock (RepoLock) - { - return Properties.Settings.Default.CommitterName; - } - } - - //public api - public void SetCommitterName(string newName) - { - lock (RepoLock) - { - Properties.Settings.Default.CommitterName = newName; - } - } - - //public api - public string GetCommitterEmail() - { - lock (RepoLock) - { - return Properties.Settings.Default.CommitterEmail; - } - } - - //public api - public void SetCommitterEmail(string newEmail) - { - lock (RepoLock) - { - Properties.Settings.Default.CommitterEmail = newEmail; - } - } - - public string SynchronizePush() - { - var Config = new RepoConfig(false); - if (Config == null) - return "Error reading changelog configuration"; - if(!Config.ChangelogSupport || !SSHAuth()) - return null; - return LocalIsRemote() ? Commit(Config) ?? Push() : "Can't push changelog: HEAD does not match tracked remote branch"; - } - - FetchOptions GenerateFetchOptions() - { - return new FetchOptions() - { - CredentialsProvider = GenerateGitCredentials, - OnTransferProgress = HandleTransferProgress, - Prune = true, - }; - } - - /// - /// Fetches origin - /// - /// null on success, error message on failure - string Fetch() - { - try - { - string logMessage = ""; - var R = Repo.Network.Remotes["origin"]; - IEnumerable refSpecs = R.FetchRefSpecs.Select(X => X.Specification); - Commands.Fetch(Repo, R.Name, refSpecs, GenerateFetchOptions(), logMessage); - return null; - } - catch (Exception e) - { - return e.ToString(); - } - } - - bool LocalIsRemote() - { - lock (RepoLock) - { - if (LoadRepo() != null) - return false; - if (Fetch() != null) - return false; - try - { - return Repo.Head.IsTracking && Repo.Head.TrackedBranch.Tip.Sha == Repo.Head.Tip.Sha; - } - catch - { - return false; - } - } - } - - string Commit(RepoConfig Config) - { - lock (RepoLock) - { - var result = LoadRepo(); - if (result != null) - return result; - try - { - // Stage the file - foreach(var I in Config.PathsToStage) - Commands.Stage(Repo, I); - - if (Repo.RetrieveStatus().Staged.Count() == 0) //nothing to commit - return null; - - // Create the committer's signature and commit - var authorandcommitter = MakeSig(); - - // Commit to the repository - TGServerService.WriteInfo(String.Format("Commit {0} created from changelogs", Repo.Commit(CommitMessage, authorandcommitter, authorandcommitter)), TGServerService.EventID.RepoCommit); - DeletePRList(); - return null; - } - catch (Exception e) - { - TGServerService.WriteError("Repo commit failed: " + e.ToString(), TGServerService.EventID.RepoCommitFail); - return e.ToString(); - } - } - } - - //public api - string Push() - { - if (LocalIsRemote()) //nothing to push - return null; - lock (RepoLock) - { - var result = LoadRepo(); - if (result != null) - return result; - try - { - if (!SSHAuth()) - return String.Format("Either {0} or {1} is missing from the server directory. Unable to push!", PrivateKeyPath, PublicKeyPath); - - var options = new PushOptions() - { - CredentialsProvider = GenerateGitCredentials, - }; - Repo.Network.Push(Repo.Network.Remotes[SSHPushRemote], Repo.Head.CanonicalName, options); - TGServerService.WriteInfo("Repo pushed up to commit: " + Repo.Head.Tip.Sha, TGServerService.EventID.RepoPush); - return null; - } - catch (Exception e) - { - TGServerService.WriteError("Repo push failed: " + e.ToString(), TGServerService.EventID.RepoPushFail); - return e.ToString(); - } - } - } - - bool SSHAuth() - { - return File.Exists(PrivateKeyPath) && File.Exists(PublicKeyPath); - } - - Credentials GenerateGitCredentials(string url, string usernameFromUrl, SupportedCredentialTypes types) - { - var user = usernameFromUrl ?? "git"; - if (types == SupportedCredentialTypes.UsernameQuery) - return new UsernameQueryCredentials() - { - Username = user, - }; - return new SshUserKeyCredentials() - { - Username = user, - PrivateKey = PrivateKeyPath, - PublicKey = PublicKeyPath, - Passphrase = "", - }; - } - - //public api - public string GenerateChangelog(out string error) - { - return GenerateChangelogImpl(out error); - } - - //impl proc just for single level recursion - public string GenerateChangelogImpl(out string error, bool recurse = false) - { - var RConfig = new RepoConfig(false); - if (RConfig == null) - { - error = null; - return "Error loading changelog config!"; - } - if (!RConfig.ChangelogSupport) - { - error = null; - return null; - } - - string ChangelogPy = RConfig.PathToChangelogPy; - if (!Exists()) - { - error = "Repo does not exist!"; - return null; - } - - lock (RepoLock) - { - if (RepoBusy) - { - error = "Repo is busy!"; - return null; - } - if (!File.Exists(Path.Combine(RepoPath, ChangelogPy))) - { - error = "Missing changelog generation script!"; - return null; - } - - var Config = Properties.Settings.Default; - - var PythonFile = Config.PythonPath + "/python.exe"; - if (!File.Exists(PythonFile)) - { - error = "Cannot locate python!"; - return null; - } - try - { - string result; - int exitCode; - using (var python = new Process()) - { - python.StartInfo.FileName = PythonFile; - python.StartInfo.Arguments = String.Format("{0} {1}", ChangelogPy, RConfig.ChangelogPyArguments); - python.StartInfo.UseShellExecute = false; - python.StartInfo.WorkingDirectory = new DirectoryInfo(RepoPath).FullName; - python.StartInfo.RedirectStandardOutput = true; - python.Start(); - using (StreamReader reader = python.StandardOutput) - { - result = reader.ReadToEnd(); - - } - python.WaitForExit(); - exitCode = python.ExitCode; - } - if (exitCode != 0) - { - if (recurse || RConfig.PipDependancies.Count == 0) - { - error = "Script failed!"; - return result; - } - //update pip deps and try again - - string PipFile = Config.PythonPath + "/scripts/pip.exe"; - foreach(var I in RConfig.PipDependancies) - using (var pip = new Process()) - { - pip.StartInfo.FileName = PipFile; - pip.StartInfo.Arguments = "install " + I; - pip.StartInfo.UseShellExecute = false; - pip.StartInfo.RedirectStandardOutput = true; - pip.Start(); - using (StreamReader reader = pip.StandardOutput) - { - result += "\r\n---BEGIN-PIP-OUTPUT---\r\n" + reader.ReadToEnd(); - } - pip.WaitForExit(); - if (pip.ExitCode != 0) - { - error = "Script and pip failed!"; - return result; - } - } - //and recurse - return GenerateChangelogImpl(out error, true); - } - error = null; - TGServerService.WriteInfo("Changelog generated" + error, TGServerService.EventID.RepoChangelog); - return result; - } - catch (Exception e) - { - error = e.ToString(); - TGServerService.WriteWarning("Changelog generation failed: " + error, TGServerService.EventID.RepoChangelogFail); - return null; - } - } - } - - /// - public void SetAutoUpdateInterval(ulong newInterval) - { - lock (autoUpdateTimer) - { - autoUpdateTimer.Stop(); - if (newInterval > 0) { - autoUpdateTimer.Interval = newInterval * 60 * 1000; //convert from minutes to ms - autoUpdateTimer.Start(); - } - } - Properties.Settings.Default.AutoUpdateInterval = newInterval; - } - - public ulong AutoUpdateInterval() - { - return Properties.Settings.Default.AutoUpdateInterval; - } - - private void AutoUpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) - { - if (UpdateImpl(true, false) == null) - { - Compile(true); - } - } - - //public api - public bool SetPythonPath(string path) - { - if (!Directory.Exists(path)) - return false; - Properties.Settings.Default.PythonPath = Path.GetFullPath(path); - return true; - } - - public string PythonPath() - { - return Properties.Settings.Default.PythonPath; - } - - void UpdateLiveSha(string newSha) - { - if (LoadRepo() != null) - return; - var B = Repo.Branches[LiveTrackingBranch]; - if (B != null) - Repo.Branches.Remove(B); - Repo.CreateBranch(LiveTrackingBranch, newSha); - } - - public string LiveSha() - { - var B = Repo.Branches[LiveTrackingBranch]; - return B != null ? B.Tip.Sha : "UNKNOWN"; - } - } -} +using LibGit2Sharp; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Web.Script.Serialization; +using TGServiceInterface; +using TGServiceInterface.Components; + +namespace TGServerService +{ + partial class ServerInstance : ITGRepository, IDisposable + { + const string RepoPath = "Repository"; + const string RepoTGS3SettingsPath = RepoPath + "/TGS3.json"; + const string CachedTGS3SettingsPath = "TGS3.json"; + const string RepoErrorUpToDate = "Already up to date!"; + const string SSHPushRemote = "ssh_push_target"; + const string RepoKeyDir = "RepoKey/"; + const string PrivateKeyPath = RepoKeyDir + "private_key.txt"; + const string PublicKeyPath = RepoKeyDir + "public_key.txt"; + const string PRJobFile = "prtestjob.json"; + const string LiveTrackingBranch = "___TGSLiveCommitTrackingBranch"; + const string CommitMessage = "Automatic changelog compile, [ci skip]"; + + object RepoLock = new object(); + bool RepoBusy = false; + bool Cloning = false; + + Repository Repo; + int currentProgress = -1; + + System.Timers.Timer autoUpdateTimer = new System.Timers.Timer() + { + AutoReset = true + }; + + /// + /// Repo specific information about the installation + /// Requires RepoLock and !RepoBusy to be instantiated + /// + class RepoConfig : IEquatable + { + public readonly bool ChangelogSupport; + public readonly string PathToChangelogPy; + public readonly string ChangelogPyArguments; + public readonly IList PipDependancies = new List(); + public readonly IList PathsToStage = new List(); + public readonly IList StaticDirectoryPaths = new List(); + public readonly IList DLLPaths = new List(); + + public RepoConfig(bool FromRepository) + { + var path = FromRepository ? RepoTGS3SettingsPath : CachedTGS3SettingsPath; + if (!File.Exists(path)) + return; + var rawdata = File.ReadAllText(path); + var Deserializer = new JavaScriptSerializer(); + var json = Deserializer.Deserialize>(rawdata); + try + { + var details = (IDictionary)json["changelog"]; + PathToChangelogPy = (string)details["script"]; + ChangelogPyArguments = (string)details["arguments"]; + ChangelogSupport = true; + try + { + PipDependancies = LoadArray(details["pip_dependancies"]); + } + catch { } + } + catch { + ChangelogSupport = false; + } + try + { + PathsToStage = LoadArray(json["synchronize_paths"]); + } + catch { } + try + { + StaticDirectoryPaths = LoadArray(json["static_directories"]); + } + catch { } + try + { + DLLPaths = LoadArray(json["dlls"]); + } + catch { } + } + private static IList LoadArray(object o) + { + var array = (object[])o; + var res = new List(); + foreach (var I in array) + res.Add((string)I); + return res; + } + + public override bool Equals(object obj) + { + return Equals(obj as RepoConfig); + } + + private static bool ListEquals(IList A, IList B) + { + return A.All(B.Contains) && A.Count == B.Count; + } + + public bool Equals(RepoConfig other) + { + return ChangelogSupport == other.ChangelogSupport + && PathToChangelogPy == other.PathToChangelogPy + && ChangelogPyArguments == other.ChangelogPyArguments + && ListEquals(PipDependancies, other.PipDependancies) + && ListEquals(PathsToStage, other.PathsToStage) + && ListEquals(StaticDirectoryPaths, other.StaticDirectoryPaths) + && ListEquals(DLLPaths, other.DLLPaths); + } + + public override int GetHashCode() + { + var hashCode = 1890628544; + hashCode = hashCode * -1521134295 + ChangelogSupport.GetHashCode(); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(PathToChangelogPy); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ChangelogPyArguments); + hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(PipDependancies); + hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(PathsToStage); + hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(StaticDirectoryPaths); + hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(DLLPaths); + return hashCode; + } + + public static bool operator ==(RepoConfig config1, RepoConfig config2) + { + return EqualityComparer.Default.Equals(config1, config2); + } + + public static bool operator !=(RepoConfig config1, RepoConfig config2) + { + return !(config1 == config2); + } + } + + void InitRepo() + { + Directory.CreateDirectory(RepoKeyDir); + if(Exists()) + UpdateInterfaceDll(false); + if(LoadRepo() == null) + DisableGarbageCollectionNoLock(); + //start the autoupdate timer + autoUpdateTimer.Elapsed += AutoUpdateTimer_Elapsed; + SetAutoUpdateInterval(Properties.Settings.Default.AutoUpdateInterval); + } + + bool RepoConfigsMatch() + { + //this should never be called while the repo is busy + RepoConfig I = null; + lock (RepoLock) + { + if (!RepoBusy && LoadRepo() == null) + I = new RepoConfig(true); + } + if (I == null) + throw new Exception("Unable to load TGS3.json from repo!"); + var J = new RepoConfig(false); + return I == J; + } + + //public api + public bool OperationInProgress() + { + lock (RepoLock) + { + return RepoBusy; + } + } + + //public api + public int CheckoutProgress() + { + return currentProgress; + } + + //Sets up the repo object + string LoadRepo() + { + if (Repo != null) + return null; + if (!Repository.IsValid(RepoPath)) + return "Repository does not exist"; + try + { + Repo = new Repository(RepoPath); + } + catch (Exception e) + { + return e.ToString(); + } + return null; + } + + //Cleans up the repo object + void DisposeRepo() + { + if (Repo != null) + { + Repo.Dispose(); + Repo = null; + } + } + + //public api + public bool Exists() + { + lock (RepoLock) + { + return !Cloning && Repository.IsValid(RepoPath); + } + } + + //Updates the currentProgress var + //no locks required because who gives a shit, it's a fucking 32-bit integer + bool HandleTransferProgress(TransferProgress progress) + { + currentProgress = (int)(((float)progress.ReceivedObjects / progress.TotalObjects) * 100) / 2; + currentProgress += (int)(((float)progress.IndexedObjects / progress.TotalObjects) * 100) / 2; + return true; + } + + //see above + void HandleCheckoutProgress(string path, int completedSteps, int totalSteps) + { + currentProgress = (int)(((float)completedSteps / totalSteps) * 100); + } + + //For the thread parameter + private class TwoStrings + { + public string a, b; + } + + //This is the thread that resets za warldo + //clones, checksout, sets up static dir + void Clone(object twostrings) + { + //busy flag set by caller + var ts = (TwoStrings)twostrings; + var RepoURL = ts.a; + var BranchName = ts.b; + try + { + SendMessage(String.Format("REPO: {2} started: Cloning {0} branch of {1} ...", BranchName, RepoURL, Repository.IsValid(RepoPath) ? "Full reset" : "Setup"), ChatMessageType.DeveloperInfo); + try + { + DisposeRepo(); + Program.DeleteDirectory(RepoPath); + DeletePRList(); + lock (configLock) + { + BackupAndDeleteStaticDirectory(); + } + + var Opts = new CloneOptions() + { + BranchName = BranchName, + RecurseSubmodules = true, + OnTransferProgress = HandleTransferProgress, + OnCheckoutProgress = HandleCheckoutProgress, + CredentialsProvider = GenerateGitCredentials, + }; + + Repository.Clone(RepoURL, RepoPath, Opts); + currentProgress = -1; + LoadRepo(); + + DisableGarbageCollectionNoLock(); + + //create an ssh remote for pushing + Repo.Network.Remotes.Add(SSHPushRemote, RepoURL.Replace("git://", "ssh://").Replace("https://", "ssh://")); + + InitialConfigureRepository(); + + SendMessage("REPO: Clone complete!", ChatMessageType.DeveloperInfo); + Service.WriteInfo("Repository {0}:{1} successfully cloned", EventID.RepoClone); + } + finally + { + currentProgress = -1; + } + } + catch (Exception e) + + { + SendMessage("REPO: Setup failed!", ChatMessageType.DeveloperInfo); + Service.WriteWarning(String.Format("Failed to clone {2}:{0}: {1}", BranchName, e.ToString(), RepoURL), EventID.RepoCloneFail); + } + finally + { + lock (RepoLock) + { + RepoBusy = false; + Cloning = false; + } + } + } + + void DisableGarbageCollectionNoLock() + { + Repo.Config.Set("gc.auto", false); + } + + void BackupAndDeleteStaticDirectory() + { + if (Directory.Exists(StaticDirs)) + { + int count = 1; + + string path = Path.GetDirectoryName(StaticBackupDir); + string newFullPath = StaticBackupDir; + + while (File.Exists(newFullPath) || Directory.Exists(newFullPath)) + { + string tempDirName = string.Format("{0}({1})", StaticBackupDir, count++); + newFullPath = Path.Combine(path, tempDirName); + } + + Program.CopyDirectory(StaticDirs, newFullPath); + } + Program.DeleteDirectory(StaticDirs); + } + + public string UpdateTGS3Json() + { + try + { + if (File.Exists(RepoTGS3SettingsPath)) + File.Copy(RepoTGS3SettingsPath, CachedTGS3SettingsPath, true); + else if (File.Exists(CachedTGS3SettingsPath)) + File.Delete(CachedTGS3SettingsPath); + } + catch(Exception e) + { + return e.ToString(); + } + return null; + } + + void InitialConfigureRepository() + { + Directory.CreateDirectory(StaticDirs); + UpdateInterfaceDll(false); + UpdateTGS3Json(); + var Config = new RepoConfig(false); //RepoBusy is set if we're here + foreach(var I in Config.StaticDirectoryPaths) + { + try + { + var source = Path.Combine(RepoPath, I); + var dest = Path.Combine(StaticDirs, I); + if (Directory.Exists(source)) + Program.CopyDirectory(source, dest); + else + Directory.CreateDirectory(dest); + } + catch + { + Service.WriteWarning("Could not setup static directory: " + I, EventID.RepoConfigurationFail); + } + } + foreach(var I in Config.DLLPaths) + { + try + { + var source = Path.Combine(RepoPath, I); + if (!File.Exists(source)) + { + Service.WriteWarning("Could not find DLL: " + I, EventID.RepoConfigurationFail); + continue; + } + var dest = Path.Combine(StaticDirs, I); + Program.CopyFileForceDirectories(source, dest, false); + } + catch + { + Service.WriteWarning("Could not setup static DLL: " + I, EventID.RepoConfigurationFail); + } + } + } + + //kicks off the cloning thread + //public api + public string Setup(string RepoURL, string BranchName) + { + lock (RepoLock) + { + if (RepoBusy) + return "Repo is busy!"; + lock (CompilerLock) + { + if (!CompilerIdleNoLock()) + return "Compiler is running!"; + } + if (DaemonStatus() != DreamDaemonStatus.Offline) + return "DreamDaemon is running!"; + if (RepoURL.Contains("ssh://") && !SSHAuth()) + return String.Format("SSH url specified but either {0} or {1} does not exist in the server directory!", PrivateKeyPath, PublicKeyPath); + RepoBusy = true; + Cloning = true; + new Thread(new ParameterizedThreadStart(Clone)) + { + IsBackground = true //make sure we don't hold up shutdown + }.Start(new TwoStrings { a = RepoURL, b = BranchName }); + return null; + } + } + + //Gets what HEAD is pointing to + string GetShaOrBranch(out string error, bool branch, bool tracked) + { + lock (RepoLock) + { + var result = LoadRepo(); + if (result != null) + { + error = result; + return null; + } + + try + { + error = null; + if (tracked && Repo.Head.TrackedBranch != null) + return Repo.Head.TrackedBranch.Tip.Sha; + return branch ? Repo.Head.FriendlyName : Repo.Head.Tip.Sha; + } + catch (Exception e) + { + error = e.ToString(); + return null; + } + } + } + + //moist shleppy noises + //public api + public string GetHead(bool useTracked, out string error) + { + return GetShaOrBranch(out error, false, useTracked); + } + + //public api + public string GetBranch(out string error) + { + return GetShaOrBranch(out error, true, false); + } + + //public api + public string GetRemote(out string error) + { + try + { + var res = LoadRepo(); + if (res != null) + { + error = res; + return null; + } + error = null; + return Repo.Network.Remotes["origin"].Url; + } + catch (Exception e) + { + error = e.ToString(); + return null; + } + } + + //calls git reset --hard on HEAD + //requires RepoLock + string ResetNoLock(Branch targetBranch) + { + try + { + if (targetBranch != null) + Repo.Reset(ResetMode.Hard, targetBranch.Tip); + else + Repo.Reset(ResetMode.Hard); + return null; + } + catch (Exception e) + { + return e.ToString(); + } + } + + //public api + public string Checkout(string sha) + { + if (sha == LiveTrackingBranch) + return "I'm sorry Dave, I'm afraid I can't do that..."; + lock (RepoLock) + { + var result = LoadRepo(); + if (result != null) + return result; + SendMessage("REPO: Checking out object: " + sha, ChatMessageType.DeveloperInfo); + try + { + if (Repo.Branches[sha] == null) + { + //see if origin has the branch + result = Fetch(); + var trackedBranch = Repo.Branches[String.Format("origin/{0}", sha)]; + if (trackedBranch != null) + { + var newBranch = Repo.CreateBranch(sha, trackedBranch.Tip); + //track it + Repo.Branches.Update(newBranch, b => b.TrackedBranch = trackedBranch.CanonicalName); + } + else if (result != null) + return result; + } + var Opts = new CheckoutOptions() + { + CheckoutModifiers = CheckoutModifiers.Force, + OnCheckoutProgress = HandleCheckoutProgress, + }; + Commands.Checkout(Repo, sha, Opts); + var res = ResetNoLock(null); + UpdateSubmodules(); + SendMessage("REPO: Checkout complete!", ChatMessageType.DeveloperInfo); + Service.WriteInfo("Repo checked out " + sha, EventID.RepoCheckout); + return res; + } + catch (Exception e) + { + SendMessage("REPO: Checkout failed!", ChatMessageType.DeveloperInfo); + Service.WriteWarning(String.Format("Repo checkout of {0} failed: {1}", sha, e.ToString()), EventID.RepoCheckoutFail); + return e.ToString(); + } + } + } + + //Merges a thing into HEAD, not even necessarily a branch + string MergeBranch(string branchname) + { + var mo = new MergeOptions() + { + OnCheckoutProgress = HandleCheckoutProgress + }; + var Result = Repo.Merge(branchname, MakeSig()); + currentProgress = -1; + switch (Result.Status) + { + case MergeStatus.Conflicts: + ResetNoLock(null); + SendMessage("REPO: Merge conflicted, aborted.", ChatMessageType.DeveloperInfo); + return "Merge conflict occurred."; + case MergeStatus.UpToDate: + return RepoErrorUpToDate; + } + return null; + } + + //public api + public string Update(bool reset) + { + return UpdateImpl(reset, true); + } + + string UpdateImpl(bool reset, bool successOnUpToDate) + { + lock (RepoLock) + { + var result = LoadRepo(); + if (result != null) + return result; + try + { + if (Repo.Head == null || !Repo.Head.IsTracking) + return "Cannot update while not on a tracked branch"; + + var res = Fetch(); + if (res != null) + return res; + + var originBranch = Repo.Head.TrackedBranch; + if (!successOnUpToDate && Repo.Head.Tip.Sha == originBranch.Tip.Sha) + return RepoErrorUpToDate; + + SendMessage(String.Format("REPO: Updating origin branch...({0})", reset ? "Hard Reset" : "Merge"), ChatMessageType.DeveloperInfo); + + if (reset) + { + var error = ResetNoLock(Repo.Head.TrackedBranch); + UpdateSubmodules(); + if (error != null) + throw new Exception(error); + DeletePRList(); + Service.WriteInfo("Repo hard updated to " + originBranch.Tip.Sha, EventID.RepoHardUpdate); + return error; + } + res = MergeBranch(originBranch.FriendlyName); + if (res != null) + throw new Exception(res); + UpdateSubmodules(); + Service.WriteInfo("Repo merge updated to " + originBranch.Tip.Sha, EventID.RepoMergeUpdate); + return null; + } + catch (Exception E) + { + SendMessage("REPO: Update failed!", ChatMessageType.DeveloperInfo); + Service.WriteWarning(String.Format("Repo{0} update failed", reset ? " hard" : ""), reset ? EventID.RepoHardUpdateFail : EventID.RepoMergeUpdateFail); + return E.ToString(); + } + } + } + + private void UpdateSubmodules() + { + var suo = new SubmoduleUpdateOptions + { + Init = true + }; + foreach (var I in Repo.Submodules) + try + { + Repo.Submodules.Update(I.Name, suo); + } + catch (Exception e) + { + //workaround for https://github.com/libgit2/libgit2/issues/3820 + //kill off the modules/ folder in .git and try again + try + { + Program.DeleteDirectory(String.Format("{0}/.git/modules/{1}", RepoPath, I.Path)); + } + catch + { + throw e; + } + Repo.Submodules.Update(I.Name, suo); + var msg = String.Format("I had to reclone submodule {0}. If this is happening a lot find a better hack or fix https://github.com/libgit2/libgit2/issues/3820!", I.Name); + SendMessage(String.Format("REPO: {0}", msg), ChatMessageType.DeveloperInfo); + Service.WriteWarning(msg, EventID.SubmoduleReclone); + } + } + + string CreateBackup() + { + try + { + lock (RepoLock) + { + var res = LoadRepo(); + if (res != null) + return res; + + //Make sure we don't already have a backup at this commit + var HEAD = Repo.Head.Tip.Sha; + foreach (var T in Repo.Tags) + if (T.Target.Sha == HEAD) + return null; + + var tagName = "TGS-Compile-Backup-" + DateTime.Now.ToString("yyyy-MM-dd--HH.mm.ss"); + var tag = Repo.ApplyTag(tagName); + + if (tag != null) + { + Service.WriteInfo("Repo backup created at tag: " + tagName + " commit: " + HEAD, EventID.RepoBackupTag); + return null; + } + throw new Exception("Tag creation failed!"); + } + } + catch (Exception e) + { + Service.WriteWarning(String.Format("Failed backup tag creation at commit {0}!", Repo.Head.Tip.Sha), EventID.RepoBackupTagFail); + return e.ToString(); + } + } + + public IDictionary ListBackups(out string error) + { + try + { + lock (RepoLock) + { + error = LoadRepo(); + if (error != null) + return null; + + var res = new Dictionary(); + foreach (var T in Repo.Tags) + if (T.FriendlyName.Contains("TGS")) + res.Add(T.FriendlyName, T.Target.Sha); + return res; + } + } + catch (Exception e) + { + error = e.ToString(); + return null; + } + } + + //public api + public string Reset(bool trackedBranch) + { + lock (RepoLock) + { + var res = LoadRepo() ?? ResetNoLock(trackedBranch ? (Repo.Head.TrackedBranch ?? Repo.Head) : Repo.Head); + if (res == null) + { + SendMessage(String.Format("REPO: Hard reset to {0}branch", trackedBranch ? "tracked " : ""), ChatMessageType.DeveloperInfo); + if (trackedBranch) + DeletePRList(); + Service.WriteInfo(String.Format("Repo branch reset{0}", trackedBranch ? " to tracked branch" : ""), trackedBranch ? EventID.RepoResetTracked : EventID.RepoReset); + return null; + } + Service.WriteWarning(String.Format("Failed to reset{0}: {1}", trackedBranch ? " to tracked branch" : "", res), trackedBranch ? EventID.RepoResetTrackedFail : EventID.RepoResetFail); + return res; + } + } + + //Makes the LibGit2Sharp sig we'll use for committing based on the configured stuff + Signature MakeSig() + { + var Config = Properties.Settings.Default; + return new Signature(new Identity(Config.CommitterName, Config.CommitterEmail), DateTimeOffset.Now); + } + + //I wonder... + void DeletePRList() + { + if (File.Exists(PRJobFile)) + try + { + File.Delete(PRJobFile); + } + catch (Exception e) + { + Service.WriteError("Failed to delete PR list: " + e.ToString(), EventID.RepoPRListError); + } + } + + //json_decode(file2text()) + IDictionary> GetCurrentPRList() + { + if (!File.Exists(PRJobFile)) + return new Dictionary>(); + var rawdata = File.ReadAllText(PRJobFile); + var Deserializer = new JavaScriptSerializer(); + return Deserializer.Deserialize>>(rawdata); + } + + //text2file(json_encode()) + void SetCurrentPRList(IDictionary> list) + { + var Serializer = new JavaScriptSerializer(); + var rawdata = Serializer.Serialize(list); + File.WriteAllText(PRJobFile, rawdata); + } + + //public api + public string MergePullRequest(int PRNumber) + { + return MergePullRequestImpl(PRNumber, false); + } + string MergePullRequestImpl(int PRNumber, bool impliedUpdate) + { + lock (RepoLock) + { + var result = LoadRepo(); + if (result != null) + return result; + SendMessage(String.Format("REPO: {1}erging PR #{0}...", PRNumber, impliedUpdate ? "Test m" : "M"), ChatMessageType.DeveloperInfo); + result = ResetNoLock(null); + if (result != null) + return result; + try + { + //only supported with github + var remoteUrl = Repo.Network.Remotes["origin"].Url; + if (!remoteUrl.Contains("github.com")) + return "Only supported with Github based repositories."; + + + var Refspec = new List(); + var PRBranchName = String.Format("pr-{0}", PRNumber); + var LocalBranchName = String.Format("pull/{0}/headrefs/heads/{1}", PRNumber, PRBranchName); + Refspec.Add(String.Format("pull/{0}/head:{1}", PRNumber, PRBranchName)); + var logMessage = ""; + + var branch = Repo.Branches[LocalBranchName]; + if (branch != null) + //Need to delete the branch first in case of rebase + Repo.Branches.Remove(branch); + + Commands.Fetch(Repo, "origin", Refspec, GenerateFetchOptions(), logMessage); //shitty api has no failure state for this + + currentProgress = -1; + + var Config = Properties.Settings.Default; + + + branch = Repo.Branches[LocalBranchName]; + if (branch == null) + { + SendMessage("REPO: PR could not be fetched. Does it exist?", ChatMessageType.DeveloperInfo); + return String.Format("PR #{0} could not be fetched. Does it exist?", PRNumber); + } + + //so we'll know if this fails + var Result = MergeBranch(LocalBranchName); + + if (Result == null) + try + { + UpdateSubmodules(); + } + catch (Exception e) + { + Result = e.ToString(); + } + + if (Result == null) + { + Service.WriteInfo(String.Format("Merged pull request #{0}", PRNumber), EventID.RepoPRMerge); + try + { + var CurrentPRs = GetCurrentPRList(); + var PRNumberString = PRNumber.ToString(); + CurrentPRs.Remove(PRNumberString); + var newPR = new Dictionary(); + + //do some excellent remote fuckery here to get the api page + var prAPI = remoteUrl; + prAPI = prAPI.Replace("/.git", ""); + prAPI = prAPI.Replace(".git", ""); + prAPI = prAPI.Replace("github.com", "api.github.com/repos"); + prAPI += "/pulls/" + PRNumberString + ".json"; + string json; + using (var wc = new WebClient()) + { + wc.Headers.Add("user-agent", "TGStationServerService"); + json = wc.DownloadString(prAPI); + } + + var Deserializer = new JavaScriptSerializer(); + var dick = Deserializer.DeserializeObject(json) as IDictionary; + var user = dick["user"] as IDictionary; + + newPR.Add("commit", branch.Tip.Sha); + newPR.Add("author", (string)user["login"]); + newPR.Add("title", (string)dick["title"]); + CurrentPRs.Add(PRNumberString, newPR); + SetCurrentPRList(CurrentPRs); + } + catch (Exception e) + { + Service.WriteError("Failed to update PR list", EventID.RepoPRListError); + return "PR Merged, JSON update failed: " + e.ToString(); + } + } + return Result; + } + catch (Exception E) + { + SendMessage("REPO: PR merge failed!", ChatMessageType.DeveloperInfo); + Service.WriteWarning(String.Format("Failed to merge pull request #{0}: {1}", PRNumber, E.ToString()), EventID.RepoPRMergeFail); + return E.ToString(); + } + } + } + + //public api + public IList MergedPullRequests(out string error) + { + lock (RepoLock) + { + var result = LoadRepo(); + if (result != null) + { + error = result; + return null; + } + try + { + var PRRawData = GetCurrentPRList(); + IList output = new List(); + foreach (var I in GetCurrentPRList()) + output.Add(new PullRequestInfo(Convert.ToInt32(I.Key), I.Value["author"], I.Value["title"], I.Value["commit"])); + error = null; + return output; + } + catch (Exception e) + { + error = e.ToString(); + return null; + } + } + } + + //public api + public string GetCommitterName() + { + lock (RepoLock) + { + return Properties.Settings.Default.CommitterName; + } + } + + //public api + public void SetCommitterName(string newName) + { + lock (RepoLock) + { + Properties.Settings.Default.CommitterName = newName; + } + } + + //public api + public string GetCommitterEmail() + { + lock (RepoLock) + { + return Properties.Settings.Default.CommitterEmail; + } + } + + //public api + public void SetCommitterEmail(string newEmail) + { + lock (RepoLock) + { + Properties.Settings.Default.CommitterEmail = newEmail; + } + } + + public string SynchronizePush() + { + var Config = new RepoConfig(false); + if (Config == null) + return "Error reading changelog configuration"; + if(!Config.ChangelogSupport || !SSHAuth()) + return null; + return LocalIsRemote() ? Commit(Config) ?? Push() : "Can't push changelog: HEAD does not match tracked remote branch"; + } + + FetchOptions GenerateFetchOptions() + { + return new FetchOptions() + { + CredentialsProvider = GenerateGitCredentials, + OnTransferProgress = HandleTransferProgress, + Prune = true, + }; + } + + /// + /// Fetches origin + /// + /// null on success, error message on failure + string Fetch() + { + try + { + string logMessage = ""; + var R = Repo.Network.Remotes["origin"]; + IEnumerable refSpecs = R.FetchRefSpecs.Select(X => X.Specification); + Commands.Fetch(Repo, R.Name, refSpecs, GenerateFetchOptions(), logMessage); + return null; + } + catch (Exception e) + { + return e.ToString(); + } + } + + bool LocalIsRemote() + { + lock (RepoLock) + { + if (LoadRepo() != null) + return false; + if (Fetch() != null) + return false; + try + { + return Repo.Head.IsTracking && Repo.Head.TrackedBranch.Tip.Sha == Repo.Head.Tip.Sha; + } + catch + { + return false; + } + } + } + + string Commit(RepoConfig Config) + { + lock (RepoLock) + { + var result = LoadRepo(); + if (result != null) + return result; + try + { + // Stage the file + foreach(var I in Config.PathsToStage) + Commands.Stage(Repo, I); + + if (Repo.RetrieveStatus().Staged.Count() == 0) //nothing to commit + return null; + + // Create the committer's signature and commit + var authorandcommitter = MakeSig(); + + // Commit to the repository + Service.WriteInfo(String.Format("Commit {0} created from changelogs", Repo.Commit(CommitMessage, authorandcommitter, authorandcommitter)), EventID.RepoCommit); + DeletePRList(); + return null; + } + catch (Exception e) + { + Service.WriteError("Repo commit failed: " + e.ToString(), EventID.RepoCommitFail); + return e.ToString(); + } + } + } + + //public api + string Push() + { + if (LocalIsRemote()) //nothing to push + return null; + lock (RepoLock) + { + var result = LoadRepo(); + if (result != null) + return result; + try + { + if (!SSHAuth()) + return String.Format("Either {0} or {1} is missing from the server directory. Unable to push!", PrivateKeyPath, PublicKeyPath); + + var options = new PushOptions() + { + CredentialsProvider = GenerateGitCredentials, + }; + Repo.Network.Push(Repo.Network.Remotes[SSHPushRemote], Repo.Head.CanonicalName, options); + Service.WriteInfo("Repo pushed up to commit: " + Repo.Head.Tip.Sha, EventID.RepoPush); + return null; + } + catch (Exception e) + { + Service.WriteError("Repo push failed: " + e.ToString(), EventID.RepoPushFail); + return e.ToString(); + } + } + } + + bool SSHAuth() + { + return File.Exists(PrivateKeyPath) && File.Exists(PublicKeyPath); + } + + Credentials GenerateGitCredentials(string url, string usernameFromUrl, SupportedCredentialTypes types) + { + var user = usernameFromUrl ?? "git"; + if (types == SupportedCredentialTypes.UsernameQuery) + return new UsernameQueryCredentials() + { + Username = user, + }; + return new SshUserKeyCredentials() + { + Username = user, + PrivateKey = PrivateKeyPath, + PublicKey = PublicKeyPath, + Passphrase = "", + }; + } + + //public api + public string GenerateChangelog(out string error) + { + return GenerateChangelogImpl(out error); + } + + //impl proc just for single level recursion + public string GenerateChangelogImpl(out string error, bool recurse = false) + { + var RConfig = new RepoConfig(false); + if (RConfig == null) + { + error = null; + return "Error loading changelog config!"; + } + if (!RConfig.ChangelogSupport) + { + error = null; + return null; + } + + string ChangelogPy = RConfig.PathToChangelogPy; + if (!Exists()) + { + error = "Repo does not exist!"; + return null; + } + + lock (RepoLock) + { + if (RepoBusy) + { + error = "Repo is busy!"; + return null; + } + if (!File.Exists(Path.Combine(RepoPath, ChangelogPy))) + { + error = "Missing changelog generation script!"; + return null; + } + + var Config = Properties.Settings.Default; + + var PythonFile = Config.PythonPath + "/python.exe"; + if (!File.Exists(PythonFile)) + { + error = "Cannot locate python!"; + return null; + } + try + { + string result; + int exitCode; + using (var python = new Process()) + { + python.StartInfo.FileName = PythonFile; + python.StartInfo.Arguments = String.Format("{0} {1}", ChangelogPy, RConfig.ChangelogPyArguments); + python.StartInfo.UseShellExecute = false; + python.StartInfo.WorkingDirectory = new DirectoryInfo(RepoPath).FullName; + python.StartInfo.RedirectStandardOutput = true; + python.Start(); + using (StreamReader reader = python.StandardOutput) + { + result = reader.ReadToEnd(); + + } + python.WaitForExit(); + exitCode = python.ExitCode; + } + if (exitCode != 0) + { + if (recurse || RConfig.PipDependancies.Count == 0) + { + error = "Script failed!"; + return result; + } + //update pip deps and try again + + string PipFile = Config.PythonPath + "/scripts/pip.exe"; + foreach(var I in RConfig.PipDependancies) + using (var pip = new Process()) + { + pip.StartInfo.FileName = PipFile; + pip.StartInfo.Arguments = "install " + I; + pip.StartInfo.UseShellExecute = false; + pip.StartInfo.RedirectStandardOutput = true; + pip.Start(); + using (StreamReader reader = pip.StandardOutput) + { + result += "\r\n---BEGIN-PIP-OUTPUT---\r\n" + reader.ReadToEnd(); + } + pip.WaitForExit(); + if (pip.ExitCode != 0) + { + error = "Script and pip failed!"; + return result; + } + } + //and recurse + return GenerateChangelogImpl(out error, true); + } + error = null; + Service.WriteInfo("Changelog generated" + error, EventID.RepoChangelog); + return result; + } + catch (Exception e) + { + error = e.ToString(); + Service.WriteWarning("Changelog generation failed: " + error, EventID.RepoChangelogFail); + return null; + } + } + } + + /// + public void SetAutoUpdateInterval(ulong newInterval) + { + lock (autoUpdateTimer) + { + autoUpdateTimer.Stop(); + if (newInterval > 0) { + autoUpdateTimer.Interval = newInterval * 60 * 1000; //convert from minutes to ms + autoUpdateTimer.Start(); + } + } + Properties.Settings.Default.AutoUpdateInterval = newInterval; + } + + public ulong AutoUpdateInterval() + { + return Properties.Settings.Default.AutoUpdateInterval; + } + + private void AutoUpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + { + if (UpdateImpl(true, false) == null) + { + Compile(true); + } + } + + //public api + public bool SetPythonPath(string path) + { + if (!Directory.Exists(path)) + return false; + Properties.Settings.Default.PythonPath = Path.GetFullPath(path); + return true; + } + + public string PythonPath() + { + return Properties.Settings.Default.PythonPath; + } + + void UpdateLiveSha(string newSha) + { + if (LoadRepo() != null) + return; + var B = Repo.Branches[LiveTrackingBranch]; + if (B != null) + Repo.Branches.Remove(B); + Repo.CreateBranch(LiveTrackingBranch, newSha); + } + + public string LiveSha() + { + var B = Repo.Branches[LiveTrackingBranch]; + return B != null ? B.Tip.Sha : "UNKNOWN"; + } + } +} diff --git a/TGServerService/InterfaceBase.cs b/TGServerService/ServerInstance/ServerInstance.cs similarity index 93% rename from TGServerService/InterfaceBase.cs rename to TGServerService/ServerInstance/ServerInstance.cs index 6728d5c78f..ef21c8ca1a 100644 --- a/TGServerService/InterfaceBase.cs +++ b/TGServerService/ServerInstance/ServerInstance.cs @@ -1,6 +1,6 @@ using System; using System.ServiceModel; -using TGServiceInterface; +using TGServiceInterface.Components; namespace TGServerService { @@ -9,12 +9,12 @@ namespace TGServerService //this line basically says make one instance of the service, use it multithreaded for requests, and never delete it [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.Single)] - partial class TGStationServer : IDisposable, ITGSService, ITGConnectivity + partial class ServerInstance : IDisposable, ITGSService, ITGConnectivity { //call partial constructors/destructors from here //called when the service is started - public TGStationServer() + public ServerInstance() { FindTheDroidsWereLookingFor(); InitChat(); @@ -37,7 +37,7 @@ namespace TGServerService //public api public string Version() { - return TGServerService.Version; + return Service.Version; } //public api diff --git a/TGServerService/ServerService.Designer.cs b/TGServerService/Service.Designer.cs similarity index 96% rename from TGServerService/ServerService.Designer.cs rename to TGServerService/Service.Designer.cs index 029f1291c9..7c2721b451 100644 --- a/TGServerService/ServerService.Designer.cs +++ b/TGServerService/Service.Designer.cs @@ -1,6 +1,6 @@ namespace TGServerService { - partial class TGServerService + sealed partial class Service { /// /// Required designer variable. diff --git a/TGServerService/ServerService.cs b/TGServerService/Service.cs similarity index 68% rename from TGServerService/ServerService.cs rename to TGServerService/Service.cs index 95552baef5..2603e49c8c 100644 --- a/TGServerService/ServerService.cs +++ b/TGServerService/Service.cs @@ -5,89 +5,14 @@ using System.Security.Principal; using System.ServiceModel; using System.ServiceProcess; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGServerService -{ - partial class TGServerService : ServiceBase - { - //only deprecate events, do not reuse them - public enum EventID - { - ChatCommand = 100, - ChatConnectFail = 200, - ChatProviderStartFail = 300, - InvalidChatProvider = 400, - UpdateRequest = 500, - BYONDUpdateFail = 600, - BYONDUpdateStaged = 700, - BYONDUpdateComplete = 800, - ServerMoveFailed = 900, - ServerMovePartial = 1000, - ServerMoveComplete = 1100, - DMCompileCrash = 1200, - DMInitializeCrash = 1300, - DMCompileError = 1400, - DMCompileSuccess = 1500, - DMCompileCancel = 1600, - DDReattachFail = 1700, - DDReattachSuccess = 1800, - DDWatchdogCrash = 1900, - DDWatchdogExit = 2000, - DDWatchdogRebootedServer = 2100, - DDWatchdogRebootingServer = 2200, - DDWatchdogRestart = 2300, - DDWatchdogRestarted = 2400, - DDWatchdogStarted = 2500, - ChatSend = 2600, - ChatBroadcast = 2700, - //ChatAdminBroadcast = 2800, - ChatDisconnectFail = 2900, - //TopicSent = 3000, - //TopicFailed = 3100, - CommsKeySet = 3200, - NudgeStartFail = 3300, - NudgeCrash = 3400, - RepoClone = 3500, - RepoCloneFail = 3600, - RepoCheckout = 3700, - RepoCheckoutFail = 3800, - RepoHardUpdate = 3900, - RepoHardUpdateFail = 4000, - RepoMergeUpdate = 4100, - RepoMergeUpdateFail = 4200, - RepoBackupTag = 4300, - RepoBackupTagFail = 4400, - RepoResetTracked = 4500, - RepoResetTrackedFail = 4600, - RepoReset = 4700, - RepoResetFail = 4800, - RepoPRListError = 4900, - RepoPRMerge = 5000, - RepoPRMergeFail = 5100, - RepoCommit = 5200, - RepoCommitFail = 5300, - RepoPush = 5400, - RepoPushFail = 5500, - RepoChangelog = 5600, - RepoChangelogFail = 5700, - ServiceShutdownFail = 6100, - WorldReboot = 6200, - ServerUpdateApplied = 6300, - ChatBroadcastFail = 6400, - IRCLogModes = 6500, - SubmoduleReclone = 6600, - Authentication = 6700, - PreactionEvent = 6800, - PreactionFail = 6900, - InteropCallException = 7000, - APIVersionMismatch = 7100, - RepoConfigurationFail = 7200, - StaticRead = 7300, - StaticWrite = 7400, - StaticDelete = 7500, - } +{//only deprecate events, do not reuse them - static TGServerService ActiveService; //So everyone else can write to our eventlog + sealed partial class Service : ServiceBase + { + static Service ActiveService; //So everyone else can write to our eventlog public static readonly string Version = "/tg/station 13 Server Service v" + FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).FileVersion; @@ -127,7 +52,7 @@ namespace TGServerService //you should seriously not add anything here //Use OnStart instead - public TGServerService() + public Service() { try { @@ -164,7 +89,7 @@ namespace TGServerService } Environment.CurrentDirectory = Config.ServerDirectory; - var instance = new TGStationServer(); + var instance = new ServerInstance(); for (var I = 0; I < args.Length - 1; ++I) if (args[I].ToLower() == "-port") @@ -226,7 +151,7 @@ namespace TGServerService { try { - TGStationServer instance = (TGStationServer)host.SingletonInstance; + var instance = (ServerInstance)host.SingletonInstance; host.Close(); instance.Dispose(); } diff --git a/TGServerService/ServerService.resx b/TGServerService/Service.resx similarity index 100% rename from TGServerService/ServerService.resx rename to TGServerService/Service.resx diff --git a/TGServerService/TGServerService.csproj b/TGServerService/TGServerService.csproj index 48c8a5632a..d5ece5b6f4 100644 --- a/TGServerService/TGServerService.csproj +++ b/TGServerService/TGServerService.csproj @@ -82,36 +82,39 @@ - - - + + + + - - - - - - - - + + + + + + + + + + True True Settings.settings - + Component ProjectInstaller.cs - + Component - - ServerService.cs + + Service.cs @@ -133,8 +136,9 @@ ProjectInstaller.cs - - ServerService.cs + + Service.cs + Designer diff --git a/TGServiceInterface/ChatProvider.cs b/TGServiceInterface/ChatSetupInfo.cs similarity index 82% rename from TGServiceInterface/ChatProvider.cs rename to TGServiceInterface/ChatSetupInfo.cs index d52a53a866..ddbe90156d 100644 --- a/TGServiceInterface/ChatProvider.cs +++ b/TGServiceInterface/ChatSetupInfo.cs @@ -5,51 +5,13 @@ using System.Web.Script.Serialization; namespace TGServiceInterface { - /// - /// The type of chat provider - /// - public enum TGChatProvider : int - { - /// - /// IRC chat provider - /// - IRC = 0, - /// - /// Discord chat provider - /// - Discord = 1, - } - - /// - /// Supported irc permission modes - /// - public enum IRCMode : int - { - /// - /// + - /// - Voice, - /// - /// % - /// - Halfop, - /// - /// @ - /// - Op, - /// - /// ~ - /// - Owner, - } - /// /// For setting up authentication no matter the chat provider /// [DataContract] - [KnownType(typeof(TGIRCSetupInfo))] - [KnownType(typeof(TGDiscordSetupInfo))] - public class TGChatSetupInfo + [KnownType(typeof(IRCSetupInfo))] + [KnownType(typeof(DiscordSetupInfo))] + public class ChatSetupInfo { const int AdminListIndex = 0; const int AdminModeIndex = 1; @@ -64,7 +26,7 @@ namespace TGServiceInterface /// protected const int BaseIndex = 8; /// - /// Set to if a child constructor should use the baseInfo parameter of to initialize it's property fields, otherwise + /// Set to if a child constructor should use the baseInfo parameter of to initialize it's property fields, otherwise /// protected readonly bool InitializeFields; @@ -75,11 +37,11 @@ namespace TGServiceInterface public IList DataFields { get; protected set; } /// - /// Constructs a from optional + /// Constructs a from optional /// /// Optional past data /// The number of fields in this chat provider - protected TGChatSetupInfo(TGChatSetupInfo baseInfo, int numFields) + protected ChatSetupInfo(ChatSetupInfo baseInfo, int numFields) { numFields += BaseIndex; InitializeFields = baseInfo == null || baseInfo.DataFields.Count != numFields; @@ -103,24 +65,24 @@ namespace TGServiceInterface } /// - /// Recreates as the correct child + /// Recreates as the correct child /// - /// A new based on the type - TGChatSetupInfo Specialize() + /// A new based on the type + ChatSetupInfo Specialize() { switch (Provider) { - case TGChatProvider.IRC: - return new TGIRCSetupInfo(this); - case TGChatProvider.Discord: - return new TGDiscordSetupInfo(this); + case ChatProvider.IRC: + return new IRCSetupInfo(this); + case ChatProvider.Discord: + return new DiscordSetupInfo(this); default: throw new Exception("Invalid provider!"); } } /// - /// Properly formats a name for the + /// Properly formats a name for the /// /// The to format /// The formatted @@ -147,10 +109,10 @@ namespace TGServiceInterface } /// - /// Constructs a from a data list + /// Constructs a from a data list /// /// The data - public TGChatSetupInfo(IList DeserializedData) + public ChatSetupInfo(IList DeserializedData) { DataFields = DeserializedData; } @@ -230,9 +192,9 @@ namespace TGServiceInterface /// /// The type of provider /// - public TGChatProvider Provider + public ChatProvider Provider { - get { return (TGChatProvider)Convert.ToInt32(DataFields[ProviderIndex]); } + get { return (ChatProvider)Convert.ToInt32(DataFields[ProviderIndex]); } set { DataFields[ProviderIndex] = Convert.ToString((int)value); } } } @@ -241,7 +203,7 @@ namespace TGServiceInterface /// Chat provider for IRC. Admin entries should be user nicknames in normal mode or required channel flags in special mode /// [DataContract] - public sealed class TGIRCSetupInfo : TGChatSetupInfo + public sealed class IRCSetupInfo : ChatSetupInfo { const int URLIndex = 0; const int PortIndex = 1; @@ -255,9 +217,9 @@ namespace TGServiceInterface /// Construct IRC setup info from optional generic info. Defaults to TGS3 on rizons IRC server /// /// Optional generic info - public TGIRCSetupInfo(TGChatSetupInfo baseInfo = null) : base(baseInfo, FieldsLen) + public IRCSetupInfo(ChatSetupInfo baseInfo = null) : base(baseInfo, FieldsLen) { - Provider = TGChatProvider.IRC; + Provider = ChatProvider.IRC; if (InitializeFields) { Nickname = "TGS3"; @@ -332,7 +294,7 @@ namespace TGServiceInterface /// Chat provider for Discord. Admin entires should be user ids in normal mode or group ids in special mode /// [DataContract] - public sealed class TGDiscordSetupInfo : TGChatSetupInfo + public sealed class DiscordSetupInfo : ChatSetupInfo { const int BotTokenIndex = 0; const int FieldsLen = 1; @@ -340,9 +302,9 @@ namespace TGServiceInterface /// Construct Discord setup info from optional generic info. Default is not a valid discord bot tokent /// /// Optional generic info - public TGDiscordSetupInfo(TGChatSetupInfo baseInfo = null) : base(baseInfo, FieldsLen) + public DiscordSetupInfo(ChatSetupInfo baseInfo = null) : base(baseInfo, FieldsLen) { - Provider = TGChatProvider.Discord; + Provider = ChatProvider.Discord; if (InitializeFields) BotToken = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; //needless to say, this is fake } diff --git a/TGServiceInterface/Command.cs b/TGServiceInterface/Command.cs index 48282a1ffb..c018a9c6b1 100644 --- a/TGServiceInterface/Command.cs +++ b/TGServiceInterface/Command.cs @@ -4,34 +4,33 @@ using System.Threading; namespace TGServiceInterface { - /// - /// Exit codes for s - /// - public enum ExitCode - { - /// - /// The ran successfully - /// - Normal = 0, - /// - /// The connection to the service was interrupted during the - /// - ConnectionError = 1, - /// - /// Invalid parameters for - /// - BadCommand = 2, - /// - /// The command failed due to conditions on the service - /// - ServerError = 3, - } - /// /// Helper for creating a text tree /// public abstract class Command { + /// + /// Exit codes for s + /// + public enum ExitCode + { + /// + /// The ran successfully + /// + Normal = 0, + /// + /// The connection to the service was interrupted during the + /// + ConnectionError = 1, + /// + /// Invalid parameters for + /// + BadCommand = 2, + /// + /// The command failed due to conditions on the service + /// + ServerError = 3, + } /// /// Proc that will show a message to the invoker. Do not call directly, use instead /// diff --git a/TGServiceInterface/Administration.cs b/TGServiceInterface/Components/Administration.cs similarity index 98% rename from TGServiceInterface/Administration.cs rename to TGServiceInterface/Components/Administration.cs index 00b66b2267..fb34c03094 100644 --- a/TGServiceInterface/Administration.cs +++ b/TGServiceInterface/Components/Administration.cs @@ -1,6 +1,6 @@ using System.ServiceModel; -namespace TGServiceInterface +namespace TGServiceInterface.Components { /// /// Manage the group that is used to access the service, can only be used by an administrator diff --git a/TGServiceInterface/Byond.cs b/TGServiceInterface/Components/Byond.cs similarity index 60% rename from TGServiceInterface/Byond.cs rename to TGServiceInterface/Components/Byond.cs index 83aa851441..d640179d6c 100644 --- a/TGServiceInterface/Byond.cs +++ b/TGServiceInterface/Components/Byond.cs @@ -1,56 +1,7 @@ using System.ServiceModel; -namespace TGServiceInterface +namespace TGServiceInterface.Components { - /// - /// The status of a BYOND update job - /// - public enum TGByondStatus - { - /// - /// No byond update in progress - /// - Idle, - /// - /// Preparing to update - /// - Starting, - /// - /// Revision is downloading - /// - Downloading, - /// - /// Revision is deflating - /// - Staging, - /// - /// Revision is ready and waiting for DreamDaemon reboot - /// - Staged, - /// - /// Revision is being applied - /// - Updating, - } - - /// - /// Type of byond version - /// - public enum TGByondVersion - { - /// - /// The highest version from http://www.byond.com/download/build/LATEST/ - /// - Latest, - /// - /// The version in the staging directory - /// - Staged, - /// - /// The installed version - /// - Installed, - } /// /// For managing the BYOND installation the server runs @@ -63,7 +14,7 @@ namespace TGServiceInterface /// /// The current status of the byond updater [OperationContract] - TGByondStatus CurrentStatus(); + ByondStatus CurrentStatus(); /// /// updates the used byond version to that of version .. The change won't take place until DD reboots. Calls if blocked by a running DD instance. Runs asyncronously, use to check progress @@ -87,6 +38,6 @@ namespace TGServiceInterface /// The type of version to retrieve /// if no version is detected, the version string otherwise [OperationContract] - string GetVersion(TGByondVersion type); + string GetVersion(ByondVersion type); } } diff --git a/TGServiceInterface/Chat.cs b/TGServiceInterface/Components/Chat.cs similarity index 72% rename from TGServiceInterface/Chat.cs rename to TGServiceInterface/Components/Chat.cs index 0868f56999..e52659d3f7 100644 --- a/TGServiceInterface/Chat.cs +++ b/TGServiceInterface/Components/Chat.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ServiceModel; -namespace TGServiceInterface +namespace TGServiceInterface.Components { /// /// Interface for handling chat bot @@ -14,13 +14,13 @@ namespace TGServiceInterface /// /// The info to set [OperationContract] - string SetProviderInfo(TGChatSetupInfo info); + string SetProviderInfo(ChatSetupInfo info); /// - /// Returns for all s + /// Returns for all s /// - /// A list of all s + /// A list of all s [OperationContract] - IList ProviderInfos(); + IList ProviderInfos(); /// /// Checks connection status @@ -28,7 +28,7 @@ namespace TGServiceInterface /// The type of provider to check if connected /// if connected, otherwise [OperationContract] - bool Connected(TGChatProvider providerType); + bool Connected(ChatProvider providerType); /// /// Reconnect a specific to it's chat service @@ -36,6 +36,6 @@ namespace TGServiceInterface /// The type of provider to reconnect /// on success, error message on failure [OperationContract] - string Reconnect(TGChatProvider providerType); + string Reconnect(ChatProvider providerType); } } diff --git a/TGServiceInterface/Compiler.cs b/TGServiceInterface/Components/Compiler.cs similarity index 79% rename from TGServiceInterface/Compiler.cs rename to TGServiceInterface/Components/Compiler.cs index 20a66b84ac..f5b261fb62 100644 --- a/TGServiceInterface/Compiler.cs +++ b/TGServiceInterface/Components/Compiler.cs @@ -1,30 +1,7 @@ using System.ServiceModel; -namespace TGServiceInterface +namespace TGServiceInterface.Components { - /// - /// The status of the compiler - /// - public enum TGCompilerStatus - { - /// - /// Game folder is broken or does not exist - /// - Uninitialized, - /// - /// Game folder is being created - /// - Initializing, - /// - /// Game folder is setup, does not imply the dmb is compiled - /// - Initialized, - /// - /// Game is being compiled - /// - Compiling, - } - /// /// For managing the Game A/B/Live folders, compiling, and hotswapping them /// @@ -39,7 +16,7 @@ namespace TGServiceInterface bool Initialize(); /// - /// Does all the necessary actions to take the revision currently in the repository and compile it to be run on the next server reboot. Requires BYOND to be set up and the to return . Runs asyncronously + /// Does all the necessary actions to take the revision currently in the repository and compile it to be run on the next server reboot. Requires BYOND to be set up and the to return . Runs asyncronously /// /// If no message for compilation start will be printed /// if the operation began, if it could not start @@ -58,7 +35,7 @@ namespace TGServiceInterface /// /// The current compiler status [OperationContract] - TGCompilerStatus GetStatus(); + CompilerStatus GetStatus(); /// /// Returns the error message of the last operation. Reading this will clear the stored value diff --git a/TGServiceInterface/Config.cs b/TGServiceInterface/Components/Config.cs similarity index 98% rename from TGServiceInterface/Config.cs rename to TGServiceInterface/Components/Config.cs index 55027423a4..587f8a5f79 100644 --- a/TGServiceInterface/Config.cs +++ b/TGServiceInterface/Components/Config.cs @@ -1,8 +1,7 @@ using System.Collections.Generic; -using System.Runtime.Serialization; using System.ServiceModel; -namespace TGServiceInterface +namespace TGServiceInterface.Components { /// /// For modifying the in game config diff --git a/TGServiceInterface/Connectivity.cs b/TGServiceInterface/Components/Connectivity.cs similarity index 91% rename from TGServiceInterface/Connectivity.cs rename to TGServiceInterface/Components/Connectivity.cs index 5957be6bd3..d3077d3dd9 100644 --- a/TGServiceInterface/Connectivity.cs +++ b/TGServiceInterface/Components/Connectivity.cs @@ -1,6 +1,6 @@ using System.ServiceModel; -namespace TGServiceInterface +namespace TGServiceInterface.Components { /// /// Used for testing connections to the service without authentication diff --git a/TGServiceInterface/DreamDaemon.cs b/TGServiceInterface/Components/DreamDaemon.cs similarity index 79% rename from TGServiceInterface/DreamDaemon.cs rename to TGServiceInterface/Components/DreamDaemon.cs index 0d17090081..b21fdda493 100644 --- a/TGServiceInterface/DreamDaemon.cs +++ b/TGServiceInterface/Components/DreamDaemon.cs @@ -1,183 +1,146 @@ -using System.ServiceModel; - -namespace TGServiceInterface -{ - /// - /// The status of the DD instance - /// - public enum TGDreamDaemonStatus - { - /// - /// Server is not running - /// - Offline, - /// - /// Server is being rebooted - /// - HardRebooting, - /// - /// Server is running - /// - Online, - } - - /// - /// DreamDaemon's security level - /// - public enum TGDreamDaemonSecurity - { - /// - /// Server is unrestricted in terms of file access and shell commands - /// - Trusted = 0, - /// - /// Server will not be able to run shell commands or access files outside it's working directory - /// - Safe, - /// - /// Server will not be able to run shell commands or access anything but temporary files - /// - Ultrasafe - } - - /// - /// Interface for managing the actual BYOND game server - /// - [ServiceContract] - public interface ITGDreamDaemon - { - /// - /// Gets the status of DreamDaemon - /// - /// The appropriate - [OperationContract] - TGDreamDaemonStatus DaemonStatus(); - - /// - /// Returns a human readable string of the current server status - /// - /// If , the status will include the server's current visibility and security levels - /// A human readable of the current server status - [OperationContract] - string StatusString(bool includeMetaInfo); - - /// - /// Check if a call to will fail. Of course, be aware of race conditions with other interfaces - /// - /// The error that would occur, otherwise - [OperationContract] - string CanStart(); - - /// - /// Starts the server if it isn't running - /// - /// on success or error message on failure - [OperationContract] - string Start(); - - /// - /// Immediately kills the server - /// - /// on success or error message on failure - [OperationContract] - string Stop(); - - /// - /// Immediately kills and restarts the server - /// - /// on success or error message on failure - [OperationContract] - string Restart(); - - /// - /// Restart the server after the currently running world reboots. Has no effect if the server isn't running - /// - [OperationContract] - void RequestRestart(); - - /// - /// Stop the server after the currently running world reboots. Has no effect if the server isn't running - /// - [OperationContract] - void RequestStop(); - - /// - /// Get the configured (not necessarily running) security level - /// - /// The configured (not necessarily running) - [OperationContract] - TGDreamDaemonSecurity SecurityLevel(); - - /// - /// Sets the security level of the server. Requires server reboot to apply. Calls . Note that anything higher than Trusted will disable interop from DD - /// - /// The new security level - /// if the change was immediately applied, otherwise and a call to was made - [OperationContract] - bool SetSecurityLevel(TGDreamDaemonSecurity level); - - /// - /// Get the configured port. Not necessarily the running port if it has since changed - /// - /// The configured port - [OperationContract] - ushort Port(); - - /// - /// Set the port to host DD on. Requires reboot to apply. Calls . - /// - /// The new port - [OperationContract] - void SetPort(ushort new_port); - - /// - /// Check if the watchdog will start when the service starts - /// - /// if autostart is enabled, otherwise - [OperationContract] - bool Autostart(); - - /// - /// Set the autostart config - /// - /// to start the watchdog with the service, to disable that functionality - [OperationContract] - void SetAutostart(bool on); - - /// - /// Check if the BYOND webclient is currently enabled for the server - /// - /// if the webclient is enabled, otherwise - [OperationContract] - bool Webclient(); - - /// - /// Set the webclient config. Calls - /// - /// to enable the byond webclient for the server, otherwise - [OperationContract] - void SetWebclient(bool on); - - /// - /// Checks if a server stop has been requested - /// - /// if has been called since the last server start, otherwise - [OperationContract] - bool ShutdownInProgress(); - - /// - /// Sends a message to everyone on the server - /// - /// The message to send - /// on success, error message on failure - [OperationContract] - string WorldAnnounce(string msg); - - /// - /// Returns the number of connected players. Requires game to use API version >= 3.1.0.1 - /// - /// The number of connected players or -1 on error - [OperationContract] - int PlayerCount(); - } -} +using System.ServiceModel; + +namespace TGServiceInterface.Components +{ + + /// + /// Interface for managing the actual BYOND game server + /// + [ServiceContract] + public interface ITGDreamDaemon + { + /// + /// Gets the status of DreamDaemon + /// + /// The appropriate + [OperationContract] + DreamDaemonStatus DaemonStatus(); + + /// + /// Returns a human readable string of the current server status + /// + /// If , the status will include the server's current visibility and security levels + /// A human readable of the current server status + [OperationContract] + string StatusString(bool includeMetaInfo); + + /// + /// Check if a call to will fail. Of course, be aware of race conditions with other interfaces + /// + /// The error that would occur, otherwise + [OperationContract] + string CanStart(); + + /// + /// Starts the server if it isn't running + /// + /// on success or error message on failure + [OperationContract] + string Start(); + + /// + /// Immediately kills the server + /// + /// on success or error message on failure + [OperationContract] + string Stop(); + + /// + /// Immediately kills and restarts the server + /// + /// on success or error message on failure + [OperationContract] + string Restart(); + + /// + /// Restart the server after the currently running world reboots. Has no effect if the server isn't running + /// + [OperationContract] + void RequestRestart(); + + /// + /// Stop the server after the currently running world reboots. Has no effect if the server isn't running + /// + [OperationContract] + void RequestStop(); + + /// + /// Get the configured (not necessarily running) security level + /// + /// The configured (not necessarily running) + [OperationContract] + DreamDaemonSecurity SecurityLevel(); + + /// + /// Sets the security level of the server. Requires server reboot to apply. Calls . Note that anything higher than Trusted will disable interop from DD + /// + /// The new security level + /// if the change was immediately applied, otherwise and a call to was made + [OperationContract] + bool SetSecurityLevel(DreamDaemonSecurity level); + + /// + /// Get the configured port. Not necessarily the running port if it has since changed + /// + /// The configured port + [OperationContract] + ushort Port(); + + /// + /// Set the port to host DD on. Requires reboot to apply. Calls . + /// + /// The new port + [OperationContract] + void SetPort(ushort new_port); + + /// + /// Check if the watchdog will start when the service starts + /// + /// if autostart is enabled, otherwise + [OperationContract] + bool Autostart(); + + /// + /// Set the autostart config + /// + /// to start the watchdog with the service, to disable that functionality + [OperationContract] + void SetAutostart(bool on); + + /// + /// Check if the BYOND webclient is currently enabled for the server + /// + /// if the webclient is enabled, otherwise + [OperationContract] + bool Webclient(); + + /// + /// Set the webclient config. Calls + /// + /// to enable the byond webclient for the server, otherwise + [OperationContract] + void SetWebclient(bool on); + + /// + /// Checks if a server stop has been requested + /// + /// if has been called since the last server start, otherwise + [OperationContract] + bool ShutdownInProgress(); + + /// + /// Sends a message to everyone on the server + /// + /// The message to send + /// on success, error message on failure + [OperationContract] + string WorldAnnounce(string msg); + + /// + /// Returns the number of connected players. Requires game to use API version >= 3.1.0.1 + /// + /// The number of connected players or -1 on error + [OperationContract] + int PlayerCount(); + } +} diff --git a/TGServiceInterface/Components/Interop.cs b/TGServiceInterface/Components/Interop.cs new file mode 100644 index 0000000000..2a4af3bcfd --- /dev/null +++ b/TGServiceInterface/Components/Interop.cs @@ -0,0 +1,19 @@ +using System.ServiceModel; + +namespace TGServiceInterface.Components +{ + /// + /// Used by DD to access the interop API with call()() + /// + [ServiceContract] + public interface ITGInterop + { + /// + /// Called from /world/ExportService(command) + /// + /// The command to run + /// on success, on failure + [OperationContract] + bool InteropMessage(string command); + } +} diff --git a/TGServiceInterface/Repository.cs b/TGServiceInterface/Components/Repository.cs similarity index 87% rename from TGServiceInterface/Repository.cs rename to TGServiceInterface/Components/Repository.cs index 7119d76927..ba0011867d 100644 --- a/TGServiceInterface/Repository.cs +++ b/TGServiceInterface/Components/Repository.cs @@ -1,51 +1,8 @@ using System.Collections.Generic; -using System.Runtime.Serialization; using System.ServiceModel; -namespace TGServiceInterface +namespace TGServiceInterface.Components { - /// - /// Information about a pull request - /// - [DataContract] - public class PullRequestInfo - { - /// - /// Construct a - /// - /// The PR number - /// The PR's author - /// The PR's title - /// The commit the PR was merged locally at - public PullRequestInfo(int number, string author, string title, string sha) - { - Number = number; - Author = author; - Title = title; - Sha = sha; - } - - /// - /// The PR number - /// - [DataMember] - public int Number { get; private set; } - /// - /// The PR's author - /// - [DataMember] - public string Author { get; private set; } - /// - /// The PR's title - /// - [DataMember] - public string Title { get; private set; } - /// - /// The commit the PR was merged locally at - /// - [DataMember] - public string Sha { get; private set; } - } /// /// Interface for managing the code repository /// diff --git a/TGServiceInterface/Service.cs b/TGServiceInterface/Components/Service.cs similarity index 92% rename from TGServiceInterface/Service.cs rename to TGServiceInterface/Components/Service.cs index 4ccaa040bc..2c6a4a69ee 100644 --- a/TGServiceInterface/Service.cs +++ b/TGServiceInterface/Components/Service.cs @@ -1,6 +1,6 @@ using System.ServiceModel; -namespace TGServiceInterface +namespace TGServiceInterface.Components { /// /// Interface for managing the service diff --git a/TGServiceInterface/Interop.cs b/TGServiceInterface/DreamDaemonBridge.cs similarity index 52% rename from TGServiceInterface/Interop.cs rename to TGServiceInterface/DreamDaemonBridge.cs index da00e31237..cd807d822a 100644 --- a/TGServiceInterface/Interop.cs +++ b/TGServiceInterface/DreamDaemonBridge.cs @@ -1,30 +1,14 @@ -using System; -using RGiesecke.DllExport; +using RGiesecke.DllExport; +using System; using System.Runtime.InteropServices; -using System.ServiceModel; +using TGServiceInterface.Components; namespace TGServiceInterface { - - /// - /// Used by DD to access the interop API with call()() - /// - [ServiceContract] - public interface ITGInterop - { - /// - /// Called from /world/ExportService(command) - /// - /// The command to run - /// on success, on failure - [OperationContract] - bool InteropMessage(string command); - } - /// /// Holds the proc that DD calls to access /// - public class DDInteropCallHolder + public sealed class DreamDaemonBridge { /// /// The proc that DD calls to access @@ -38,11 +22,11 @@ namespace TGServiceInterface try { var channel = Server.CreateChannel(); - try - { - channel.CreateChannel().InteropMessage(String.Join(" ", args)); - } - catch { } + try + { + channel.CreateChannel().InteropMessage(String.Join(" ", args)); + } + catch { } Server.CloseChannel(channel); } catch { } diff --git a/TGServiceInterface/Enumerations.cs b/TGServiceInterface/Enumerations.cs new file mode 100644 index 0000000000..155af7f65f --- /dev/null +++ b/TGServiceInterface/Enumerations.cs @@ -0,0 +1,148 @@ +namespace TGServiceInterface +{ + /// + /// The status of a BYOND update job + /// + public enum ByondStatus + { + /// + /// No byond update in progress + /// + Idle, + /// + /// Preparing to update + /// + Starting, + /// + /// Revision is downloading + /// + Downloading, + /// + /// Revision is deflating + /// + Staging, + /// + /// Revision is ready and waiting for DreamDaemon reboot + /// + Staged, + /// + /// Revision is being applied + /// + Updating, + } + /// + /// Type of byond version + /// + public enum ByondVersion + { + /// + /// The highest version from http://www.byond.com/download/build/LATEST/ + /// + Latest, + /// + /// The version in the staging directory + /// + Staged, + /// + /// The installed version + /// + Installed, + } + /// + /// The type of chat provider + /// + public enum ChatProvider : int + { + /// + /// IRC chat provider + /// + IRC = 0, + /// + /// Discord chat provider + /// + Discord = 1, + } + + /// + /// Supported irc permission modes + /// + public enum IRCMode : int + { + /// + /// + + /// + Voice, + /// + /// % + /// + Halfop, + /// + /// @ + /// + Op, + /// + /// ~ + /// + Owner, + } + /// + /// The status of the compiler + /// + public enum CompilerStatus + { + /// + /// Game folder is broken or does not exist + /// + Uninitialized, + /// + /// Game folder is being created + /// + Initializing, + /// + /// Game folder is setup, does not imply the dmb is compiled + /// + Initialized, + /// + /// Game is being compiled + /// + Compiling, + } + + /// + /// The status of the DD instance + /// + public enum DreamDaemonStatus + { + /// + /// Server is not running + /// + Offline, + /// + /// Server is being rebooted + /// + HardRebooting, + /// + /// Server is running + /// + Online, + } + + /// + /// DreamDaemon's security level + /// + public enum DreamDaemonSecurity + { + /// + /// Server is unrestricted in terms of file access and shell commands + /// + Trusted = 0, + /// + /// Server will not be able to run shell commands or access files outside it's working directory + /// + Safe, + /// + /// Server will not be able to run shell commands or access anything but temporary files + /// + Ultrasafe + } +} diff --git a/TGServiceInterface/PullRequestInfo.cs b/TGServiceInterface/PullRequestInfo.cs new file mode 100644 index 0000000000..6f6635ab7b --- /dev/null +++ b/TGServiceInterface/PullRequestInfo.cs @@ -0,0 +1,47 @@ +using System.Runtime.Serialization; + +namespace TGServiceInterface +{ + /// + /// Information about a pull request + /// + [DataContract] + public sealed class PullRequestInfo + { + /// + /// Construct a + /// + /// The PR number + /// The PR's author + /// The PR's title + /// The commit the PR was merged locally at + public PullRequestInfo(int number, string author, string title, string sha) + { + Number = number; + Author = author; + Title = title; + Sha = sha; + } + + /// + /// The PR number + /// + [DataMember] + public int Number { get; private set; } + /// + /// The PR's author + /// + [DataMember] + public string Author { get; private set; } + /// + /// The PR's title + /// + [DataMember] + public string Title { get; private set; } + /// + /// The commit the PR was merged locally at + /// + [DataMember] + public string Sha { get; private set; } + } +} diff --git a/TGServiceInterface/RootCommand.cs b/TGServiceInterface/RootCommand.cs index ac179e7fed..a10832df11 100644 --- a/TGServiceInterface/RootCommand.cs +++ b/TGServiceInterface/RootCommand.cs @@ -21,7 +21,7 @@ namespace TGServiceInterface /// Forward parameters to commands further down the tree /// /// List of parameters passed to the - /// The result of a sub or an appropriate if the handled it + /// The result of a sub or an appropriate if the handled it protected override ExitCode Run(IList parameters) { if (parameters.Count > 0) diff --git a/TGServiceInterface/Server.cs b/TGServiceInterface/Server.cs index 835a04f992..592a774fcd 100644 --- a/TGServiceInterface/Server.cs +++ b/TGServiceInterface/Server.cs @@ -7,6 +7,7 @@ using System.Net.Security; using System.Reflection; using System.Security.Principal; using System.ServiceModel; +using TGServiceInterface.Components; namespace TGServiceInterface { @@ -16,7 +17,7 @@ namespace TGServiceInterface public class Server { /// - /// List of s that can be used with GetComponen + /// List of s that can be used with and /// public static readonly IList ValidInterfaces = CollectComponents(); @@ -59,7 +60,7 @@ namespace TGServiceInterface /// /// Associated list of open s keyed by type. A in this list may close or fault at any time. Must be locked before being accessed /// - static Dictionary ChannelFactoryCache = new Dictionary(); + static IDictionary ChannelFactoryCache = new Dictionary(); /// /// Returns a of s that can be used with the service @@ -70,7 +71,7 @@ namespace TGServiceInterface //find all interfaces in this assembly in this namespace that have the service contract attribute var query = from t in Assembly.GetExecutingAssembly().GetTypes() where t.IsInterface - && t.Namespace == typeof(Server).Namespace + && t.Namespace == typeof(ITGSService).Namespace && t.GetCustomAttribute(typeof(ServiceContractAttribute)) != null select t; return query.ToList(); diff --git a/TGServiceInterface/TGServiceInterface.csproj b/TGServiceInterface/TGServiceInterface.csproj index d20d665cb1..75c5de13ff 100644 --- a/TGServiceInterface/TGServiceInterface.csproj +++ b/TGServiceInterface/TGServiceInterface.csproj @@ -47,23 +47,26 @@ - - - + + + - - - - - + + + + + + + - + + - - + +