diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000000..f750428a65 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,160 @@ +# CONTRIBUTING + +## Introduction + +Hello and welcome to /tg/station servers's contributing page. You are here because you are curious or interested in contributing - thank you! Everyone is free to contribute to this project as long as they follow the simple guidelines and specifications below; at /tg/station, we strive to maintain code stability and maintainability, and to do that, we need all pull requests to hold up to those specifications. It's in everyone's best interests - including yours! - if the same bug doesn't have to be fixed twice because of duplicated code. + +First things first, we want to make it clear how you can contribute (if you've never contributed before), as well as the kinds of powers the team has over your additions, to avoid any unpleasant surprises if your pull request is closed for a reason you didn't foresee. + +## Getting Started + +/tg/station doesn't have a list of goals and features to add; we instead allow freedom for contributors to suggest and create their ideas for the server. That doesn't mean we aren't determined to squash bugs, which unfortunately pop up a lot due to the deep complexity of the game. Here are some useful starting guides, if you want to contribute or if you want to know what challenges you can tackle with zero knowledge about the game's code structure. + +If you want to contribute the first thing you'll need to do is [set up Git](http://tgstation13.org/wiki/Setting_up_git) so you can download the source code. + +There is an open list of approachable issues for [your inspiration here](https://github.com/tgstation/tgstation-server/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22). + +Here is a link to the code's always up-to-date documentation: https://tgstation.github.io/tgstation-server/annotated.html + +You can of course, as always, ask for help at [#coderbus](irc://irc.rizon.net/coderbus) on irc.rizon.net. We're just here to have fun and help out, so please don't expect professional support. + +### Development Environment + +We reccommend any Visual Studio version that can support the .NET framework v4.5.2. However, the project should be buildable with any C# compiler than can read .sln files. Once installed, simply double-click TGStationServer3.sln to open it. + +#### Installing Dependencies + +Visual Studio comes with the nuget package manager. To install the dependencies, right-click the solution and select `Restore NuGet Packages`. If you are using some other development environment, you can download nuget [here](https://dist.nuget.org/win-x86-commandline/latest/nuget.exe) as a single CLI executable. Then simply run `nuget restore TGStationServer3.sln` from the root of the project directory. + +##### (Optional) Installing WiX Toolset and Visual Studio Extension + +The [WiX Toolset](http://wixtoolset.org/) is used for creating the installer .msi (Not the .exe, which is a standard C# program wrapper to the .msi). Building and modifying this is not required for debugging and development of the service but necessary if you want to debug tweaks to the installer configuration. You can download the Wix Toolset and Visual studio extension [here](http://wixtoolset.org/releases/). This will allow you to build `TGServiceInstaller.wixproj` just like all the other projects. + +#### Debugging + +So you've built the project and everything's good, right? Now you have to debug it. Debugging the command line and control panel are easy enough, just launch them like any other process. Debugging the TGServerService itself though requires a bit of finagling due to how Windows services work. + +1. Uninstall any release versions of TG Station Server 3 you may have on your machine +1. Open an administrative Windows cmd prompt +1. Run `sc delete "TG Station Server"` for sanity +1. Build the service in debug mode +1. Navigate to `C:\Windows\Microsoft.NET\Framework\v4.0.30319` +1. Run `InstallUtil.exe` with the path to your debug `TGServerService.exe` as an argument. This will register your debug build as a Windows service. + +If the command runs successfully you're all set up. Now, here is the debugging process. + +1. BEFORE BUILDING. Stop `TG Station Server` from the Windows Services control panel +1. Build your new Debug version +1. Set your breakpoints +1. Start the service +1. Use your environment to attach to `TGServerService.exe` for debugging +1. If you need to debug startup, you'll have to add `System.Diagnostics.Debugger.Start()` where you want the service to wait for you. Note that breaking in the constructor of `TGServerService.Service` will cause Windows to terminate the process if you don't move fast enough due to it thinking the service is malfunctioning. In fact, if you need to debug the constructor, it's better to launch `TGServerService.exe` as a regular debug session, you won't be able to pass the `Service.Run()` call that way, however. + +Now be careful while debugging. The service runs with root level privileges and you wouldn't want any [accidents](http://i.imgur.com/zvGEpJD.png) to happen, would you? + +## Meet the Team + +**Headcoder** + +The Headcoder is responsible for controlling, adding, and removing maintainers from the project. In addition to filling the role of a normal maintainer, they have sole authority on who becomes a maintainer, as well as who remains a maintainer and who does not. + +**Maintainers** + +Maintainers are quality control. If a proposed pull request doesn't meet the following specifications, they can request you to change it, or simply just close the pull request. Maintainers are required to give a reason for closing the pull request. + +Maintainers can revert your changes if they feel they are not worth maintaining or if they did not live up to the quality specifications. + +## Specifications + +As mentioned before, you are expected to follow these specifications in order to make everyone's lives easier. It'll save both your time and ours, by making sure you don't have to make any changes and we don't have to ask you to. Thank you for reading this section! + +### Object Oriented Code +As C# is an object-oriented language, code must be object-oriented when possible in order to be more flexible when adding content to it. If you don't know what "object-oriented" means, we highly recommend you do some light research to grasp the basics. + +### Tabs, not spaces +You must use tabs to indent your code, NOT SPACES. + +(You may use spaces to align something, but you should tab to the block level first, then add the remaining spaces) + +### No hacky code +Hacky code, such as adding specific checks, is highly discouraged and only allowed when there is ***no*** other option. (Protip: 'I couldn't immediately think of a proper way so thus there must be no other option' is not gonna cut it here! If you can't think of anything else, say that outright and admit that you need help with it. Maintainers exist for exactly that reason.) + +You can avoid hacky code by using object-oriented methodologies, such as overriding a function (called "procs" in DM) or sectioning code into functions and then overriding them as required. + +### No duplicated code +Copying code from one place to another may be suitable for small, short-time projects, but /tg/station is a long-term project and highly discourages this. + +Instead you can use object orientation, or simply placing repeated code in a function, to obey this specification easily. + +### No magic numbers or strings +This means stuff like having a "mode" variable for an object set to "1" or "2" with no clear indicator of what that means. Make these #defines with a name that more clearly states what it's for. This is clearer and enhances readability of your code! Get used to doing it! + +### Do not commit modifications to Version.cs +This file will be updated by maintainers when they deem it prudent to release a new version + +### Formatting + +The formatting style is the same one Visual studio uses by default. Quick formatting of code blocks can be achieved by deleting and retyping the trailing `}` + +### Use early return +Do not enclose a function in an if-block when returning on a condition is more feasible +This is bad: +```C# +void Hello() +{ + if (thing1) + if (!thing2) + if (thing3 == 30) + do stuff +} +``` +This is good: +```C# +void Hello() +{ + if (!thing1) + return; + if (thing2) + return; + if (thing3 != 30) + return; + do stuff +} +``` +This prevents nesting levels from getting deeper then they need to be. + +### Other Notes +* Code should be modular where possible; if you are working on a new addition, then strongly consider putting it in its own file unless it makes sense to put it with similar ones. + +* Bloated code may be necessary to add a certain feature, which means there has to be a judgement over whether the feature is worth having or not. You can help make this decision easier by making sure your code is modular. + +* You are expected to help maintain the code that you add, meaning that if there is a problem then you are likely to be approached in order to fix any issues, runtimes, or bugs. + +* If you used regex to replace code during development of your code, post the regex in your PR for the benefit of future developers and downstream users. + +## Pull Request Process + +There is no strict process when it comes to merging pull requests. Pull requests will sometimes take a while before they are looked at by a maintainer; the bigger the change, the more time it will take before they are accepted into the code. Every team member is a volunteer who is giving up their own time to help maintain and contribute, so please be courteous and respectful. Here are some helpful ways to make it easier for you and for the maintainers when making a pull request. + +* Make sure your pull request complies to the requirements outlined in [this guide](http://tgstation13.org/wiki/Getting_Your_Pull_Accepted) (with the exception of point 3) + +* You are going to be expected to document all your changes in the pull request and add/update XML documentation comments for the functions and classes you modify. Failing to do so will mean delaying it as we will have to question why you made the change. On the other hand, you can speed up the process by making the pull request readable and easy to understand, with diagrams or before/after data. + +* If you are proposing multiple changes, which change many different aspects of the code, you are expected to section them off into different pull requests in order to make it easier to review them and to deny/accept the changes that are deemed acceptable. + +* If your pull request is accepted, the code you add no longer belongs exclusively to you but to everyone; everyone is free to work on it, but you are also free to support or object to any changes being made, which will likely hold more weight, as you're the one who added the feature. It is a shame this has to be explicitly said, but there have been cases where this would've saved some trouble. + +* Please explain why you are submitting the pull request, and how you think your change will be beneficial to the server. Failure to do so will be grounds for rejecting the PR. + +* Commits MUST be properly titled and commented as we only use merge commits for the pull request process + +## Banned content +Do not add any of the following in a Pull Request or risk getting the PR closed: +* National Socialist Party of Germany content, National Socialist Party of Germany related content, or National Socialist Party of Germany references + +Just becuase something isn't on this list doesn't mean that it's acceptable. Use common sense above all else. + +## A word on Git +Yes, we know that the files have a tonne of mixed Windows and Linux line endings. Attempts to fix this have been met with less than stellar success, and as such we have decided to give up caring until there comes a time when it matters. + +Therefore, EOF settings of main repo are forbidden territory one must avoid wandering into, at risk of losing body and/or mind to the Git gods. diff --git a/.travis.yml b/.travis.yml index 4085bed57e..2b7a57c3d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ env: global: - BYOND_MAJOR="511" - BYOND_MINOR="1385" - - DMEName="DMAPITravisTester.dme" + - DMEName="Tools/DMAPITravisTester.dme" cache: directories: @@ -18,7 +18,7 @@ addons: - libstdc++6:i386 install: - - ./install_byond.sh + - ./Tools/install_byond.sh script: - - ./build_byond.sh + - ./Tools/build_byond.sh diff --git a/DMAPITravisTester.dme b/DMAPITravisTester.dme deleted file mode 100644 index e1f4f19dac..0000000000 --- a/DMAPITravisTester.dme +++ /dev/null @@ -1,21 +0,0 @@ -// DM Environment file for DMAPITravisTester.dme. -// All manual changes should be made outside the BEGIN_ and END_ blocks. -// New source code should be placed in .dm files: choose File/New --> Code File. - -// BEGIN_INTERNALS -// END_INTERNALS - -// BEGIN_FILE_DIR -#define FILE_DIR . -// END_FILE_DIR - -// BEGIN_PREFERENCES -// END_PREFERENCES - -// BEGIN_INCLUDE -#include "Config.dm" -#include "DMAPI\server_tools.dm" -#include "DMAPI\st_commands.dm" -#include "DMAPI\st_interface.dm" -#include "Test.dm" -// END_INCLUDE diff --git a/README.md b/README.md index ffdfa39f19..0002660659 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Requires python 2.7/3.6 to be installed for changelog generation ## Installing (GUI): 1. Launch TGControlPanel.exe as an administrator. A shortcut can be found on your desktop -1. Optionally switch to the `Server` tab and change the Server Path location from C:\tgstation-server-3 to wherever you wish +1. Optionally switch to the `Server` tab and change the Server Path location from C:\\tgstation-server-3 to wherever you wish 1. Go to the `Repository` Tab and set the remote address and branch of the git you with to track 1. Hit the clone button 1. While waiting go to the BYOND tab and install the BYOND version you wish @@ -73,7 +73,7 @@ This process is identical to the above steps in command line mode. You can alway 1. Obtain an SSL certificate to secure the connection (this is beyond the scope of this guide) 1. Either stick with the default port `38607` or change it with `admin set-port ` -1. [Bind the SSL certificate to port 38607](https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/how-to-configure-a-port-with-an-ssl-certificate) +1. [Bind the SSL certificate to the port](https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/how-to-configure-a-port-with-an-ssl-certificate) - e.g. `netsh http add sslcert ipport=0.0.0.0: certhash= appid={F32EDA25-0855-411C-AF5E-F0D042917E2D}` - The `appid` GUID actually doesn't matter, but for sanity, you should use the GUID of TGServerService.exe as printed above 1. Ensure the port can be acccessed from the internet @@ -97,6 +97,9 @@ The service supports updates while running a DreamDaemon instance. Simply instal * This is a symbolic link pointing to current "live" folder. * When the server is updated, we just point this to the updating folder so that the update takes place next round. +* `Diagnostics` + * This contains various timestamped diagnostic information for DreamDaemon invocations + * `Repository/` * This contains the actual git repository, all changes in here will be overwritten during update operations. @@ -118,6 +121,9 @@ The service supports updates while running a DreamDaemon instance. Simply instal * `prtestjob.json` * This contains information about current test merged pull requests in the Repository folder +* `TGS3.json` + * This is a copy of TGS3.json from the Repository. If a repostory change creates differences between the two, update operations will be blocked until the user confirms they want to change it + ### Starting the game server: To run the game server, open the `Server` tab of the control panel and click either `Start` @@ -168,7 +174,7 @@ You can clear all active test merges using `Reset to Origin Branch` in the `Repo ### Viewing Server Logs * Logs are stored in the Windows event viewer under `Windows Logs` -> `Application`. You'll need to filter this list for `TG Station Server` -* Every event type is keyed with an ID. A complete listing of these IDs can be found [here](https://github.com/tgstation/tgstation-server/blob/master/TGServerService/ServerService.cs#L15). +* Every event type is keyed with an ID. A complete listing of these IDs and their purpose can be found [here](https://github.com/tgstation/tgstation-server/blob/master/TGServerService/EventID.cs). * You can also import the custom view `View TGS3 Logs.xml` in this folder to have them automatically filtered ### Enabling upstream changelog generation @@ -180,7 +186,7 @@ You can clear all active test merges using `Reset to Origin Branch` in the `Repo ## CONTRIBUTING -* Version numbers and releases will be handled by maintainers, do not modify these in your PR +* See [CONTRIBUTING.md](https://github.com/tgstation/tgstation-server/blob/master/.github/CONTRIBUTING.md) ## LICENSING diff --git a/TGCommandLine/AdminCommands.cs b/TGCommandLine/AdminCommands.cs index 132ee6b395..b50ef3201f 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 { @@ -27,7 +28,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var res = Server.GetComponent().MoveServer(parameters[0]); + var res = Interface.GetComponent().MoveServer(parameters[0]); OutputProc(res ?? "Success"); return res == null ? ExitCode.Normal : ExitCode.ServerError; } @@ -51,7 +52,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var res = Server.GetComponent().RecreateStaticFolder(); + var res = Interface.GetComponent().RecreateStaticFolder(); OutputProc(res ?? "Success"); return res == null ? ExitCode.Normal : ExitCode.ServerError; } @@ -91,7 +92,7 @@ namespace TGCommandLine OutputProc("Invalid port number!"); return ExitCode.BadCommand; } - var res = Server.GetComponent().SetRemoteAccessPort(port); + var res = Interface.GetComponent().SetRemoteAccessPort(port); OutputProc(res ?? "Success!"); return ExitCode.Normal; } @@ -109,7 +110,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var port = Server.GetComponent().RemoteAccessPort(); + var port = Interface.GetComponent().RemoteAccessPort(); OutputProc(String.Format("{0}", port)); return ExitCode.Normal; } @@ -128,7 +129,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var group = Server.GetComponent().GetCurrentAuthorizedGroup(); + var group = Interface.GetComponent().GetCurrentAuthorizedGroup(); OutputProc(group ?? "ERROR"); return group != null ? ExitCode.Normal : ExitCode.ServerError; } @@ -153,7 +154,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var result = Server.GetComponent().SetAuthorizedGroup(parameters[0]); + var result = Interface.GetComponent().SetAuthorizedGroup(parameters[0]); if(result != null) { OutputProc("Group set to: " + result); @@ -181,7 +182,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var res = Server.GetComponent().SetAuthorizedGroup(null); + var res = Interface.GetComponent().SetAuthorizedGroup(null); if(res != "ADMIN") { OutputProc("Failed to clear the group??? We are currently set to: " + res); diff --git a/TGCommandLine/BYONDCommands.cs b/TGCommandLine/BYONDCommands.cs index 1a0e9aa27d..12b0c1a420 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,13 +27,13 @@ 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; - OutputProc(Server.GetComponent().GetVersion(type) ?? "Unistalled"); + type = ByondVersion.Latest; + OutputProc(Interface.GetComponent().GetVersion(type) ?? "Unistalled"); return ExitCode.Normal; } public override string GetArgumentString() @@ -55,24 +56,24 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - switch (Server.GetComponent().CurrentStatus()) + switch (Interface.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: @@ -108,7 +109,7 @@ namespace TGCommandLine return ExitCode.BadCommand; } - var BYOND = Server.GetComponent(); + var BYOND = Interface.GetComponent(); if (!BYOND.UpdateToVersion(Major, Minor)) { @@ -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..b5b8ee41e4 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() { @@ -47,8 +48,8 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var Chat = Server.GetComponent(); - Chat.SetProviderInfo(new TGIRCSetupInfo(Chat.ProviderInfos()[(int)TGChatProvider.IRC]) + var Chat = Interface.GetComponent(); + 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; @@ -77,7 +78,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var IRC = Server.GetComponent(); + var IRC = Interface.GetComponent(); var info = IRC.ProviderInfos()[providerIndex]; IList channels; @@ -138,7 +139,7 @@ namespace TGCommandLine class ChatPartCommand : Command { readonly int providerIndex; - public ChatPartCommand(TGChatProvider pI) + public ChatPartCommand(ChatProvider pI) { Keyword = "part"; RequiredParameters = 2; @@ -155,7 +156,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var IRC = Server.GetComponent(); + var IRC = Interface.GetComponent(); var info = IRC.ProviderInfos()[providerIndex]; IList channels; @@ -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; @@ -221,17 +222,17 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var info = Server.GetComponent().ProviderInfos()[providerIndex]; + var info = Interface.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; @@ -280,7 +281,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var res = Server.GetComponent().Reconnect(providerIndex); + var res = Interface.GetComponent().Reconnect(providerIndex); if (res != null) { OutputProc("Error: " + res); @@ -292,7 +293,7 @@ namespace TGCommandLine class ChatAddminCommand : Command { readonly int providerIndex; - public ChatAddminCommand(TGChatProvider pI) + public ChatAddminCommand(ChatProvider pI) { Keyword = "addmin"; RequiredParameters = 1; @@ -309,11 +310,11 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var IRC = Server.GetComponent(); + var IRC = Interface.GetComponent(); 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; @@ -354,8 +355,8 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var IRC = Server.GetComponent(); - var info = IRC.ProviderInfos()[(int)TGChatProvider.IRC]; + var IRC = Interface.GetComponent(); + var info = IRC.ProviderInfos()[(int)ChatProvider.IRC]; var lowerparam = parameters[0].ToLower(); if (lowerparam == "channel-mode") info.AdminsAreSpecial = true; @@ -393,8 +394,8 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var IRC = Server.GetComponent(); - var info = IRC.ProviderInfos()[(int)TGChatProvider.Discord]; + var IRC = Interface.GetComponent(); + var info = IRC.ProviderInfos()[(int)ChatProvider.Discord]; var lowerparam = parameters[0].ToLower(); if (lowerparam == "role-id") info.AdminsAreSpecial = true; @@ -432,8 +433,8 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var IRC = Server.GetComponent(); - var info = new TGIRCSetupInfo(IRC.ProviderInfos()[(int)TGChatProvider.IRC]); + var IRC = Interface.GetComponent(); + 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; @@ -481,11 +482,11 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var IRC = Server.GetComponent(); + var IRC = Interface.GetComponent(); 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; @@ -528,8 +529,8 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var IRC = Server.GetComponent(); - IRC.SetProviderInfo(new TGIRCSetupInfo(IRC.ProviderInfos()[(int)TGChatProvider.IRC]) + var IRC = Interface.GetComponent(); + IRC.SetProviderInfo(new IRCSetupInfo(IRC.ProviderInfos()[(int)ChatProvider.IRC]) { AuthTarget = parameters[0], AuthMessage = parameters[1] @@ -550,8 +551,8 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var IRC = Server.GetComponent(); - IRC.SetProviderInfo(new TGIRCSetupInfo(IRC.ProviderInfos()[(int)TGChatProvider.IRC]) + var IRC = Interface.GetComponent(); + 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; @@ -574,7 +575,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var IRC = Server.GetComponent(); + var IRC = Interface.GetComponent(); var info = IRC.ProviderInfos()[providerIndex]; OutputProc("Currently configured channels:"); OutputProc("Admin:"); @@ -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; @@ -609,7 +610,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var Chat = Server.GetComponent(); + var Chat = Interface.GetComponent(); var info = Chat.ProviderInfos()[providerIndex]; info.Enabled = true; var res = Chat.SetProviderInfo(info); @@ -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; @@ -637,7 +638,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var Chat = Server.GetComponent(); + var Chat = Interface.GetComponent(); var info = Chat.ProviderInfos()[providerIndex]; info.Enabled = false; var res = Chat.SetProviderInfo(info); @@ -674,8 +675,8 @@ namespace TGCommandLine OutputProc("Invalid parameter!"); return ExitCode.BadCommand; } - var Chat = Server.GetComponent(); - var PI = new TGIRCSetupInfo(Chat.ProviderInfos()[(int)TGChatProvider.IRC]) + var Chat = Interface.GetComponent(); + var PI = new IRCSetupInfo(Chat.ProviderInfos()[(int)ChatProvider.IRC]) { URL = splits[0] }; @@ -712,8 +713,8 @@ 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 Chat = Interface.GetComponent(); + 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..16192311e4 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 { @@ -27,7 +28,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var res = Server.GetComponent().DeleteFile(parameters[0], out bool unauthorized); + var res = Interface.GetComponent().DeleteFile(parameters[0], out bool unauthorized); if (res != null) { OutputProc(res); @@ -63,7 +64,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var list = Server.GetComponent().ListStaticDirectory(parameters.Count > 0 ? parameters[0] : null, out string error, out bool unauthorized); + var list = Interface.GetComponent().ListStaticDirectory(parameters.Count > 0 ? parameters[0] : null, out string error, out bool unauthorized); if(list == null) { OutputProc(error); @@ -87,7 +88,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - OutputProc(Server.GetComponent().ServerDirectory()); + OutputProc(Interface.GetComponent().ServerDirectory()); return ExitCode.Normal; } @@ -107,7 +108,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var bytes = Server.GetComponent().ReadText(parameters[0], parameters.Count > 2 && parameters[2].ToLower() == "--repo", out string error, out bool unauthorized); + var bytes = Interface.GetComponent().ReadText(parameters[0], parameters.Count > 2 && parameters[2].ToLower() == "--repo", out string error, out bool unauthorized); if(bytes == null) { OutputProc("Error: " + error); @@ -147,7 +148,7 @@ namespace TGCommandLine { try { - var res = Server.GetComponent().WriteText(parameters[0], File.ReadAllText(parameters[1]), out bool unauthorized); + var res = Interface.GetComponent().WriteText(parameters[0], File.ReadAllText(parameters[1]), out bool unauthorized); if (res != null) { OutputProc("Error: " + res); diff --git a/TGCommandLine/DDCommands.cs b/TGCommandLine/DDCommands.cs index 8d263f12f5..e14f763eae 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 { @@ -37,7 +38,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var res = Server.GetComponent().WorldAnnounce(String.Join(" ", parameters)); + var res = Interface.GetComponent().WorldAnnounce(String.Join(" ", parameters)); OutputProc(res ?? "Success!"); return res == null ? ExitCode.Normal : ExitCode.ServerError; } @@ -57,7 +58,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var res = Server.GetComponent().Start(); + var res = Interface.GetComponent().Start(); OutputProc(res ?? "Success!"); return res == null ? ExitCode.Normal : ExitCode.ServerError; } @@ -81,10 +82,10 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var DD = Server.GetComponent(); + var DD = Interface.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; @@ -110,10 +111,10 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var DD = Server.GetComponent(); + var DD = Interface.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; @@ -145,7 +146,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var DD = Server.GetComponent(); + var DD = Interface.GetComponent(); OutputProc(DD.StatusString(true)); if (DD.ShutdownInProgress()) OutputProc("The server will shutdown once the current round completes."); @@ -166,7 +167,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var DD = Server.GetComponent(); + var DD = Interface.GetComponent(); switch (parameters[0].ToLower()) { case "on": @@ -204,7 +205,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var DD = Server.GetComponent(); + var DD = Interface.GetComponent(); switch (parameters[0].ToLower()) { case "on": @@ -254,7 +255,7 @@ namespace TGCommandLine return ExitCode.BadCommand; } - Server.GetComponent().SetPort(port); + Interface.GetComponent().SetPort(port); return ExitCode.Normal; } @@ -279,25 +280,25 @@ 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!"); return ExitCode.BadCommand; } - Server.GetComponent().SetSecurityLevel(sec); + Interface.GetComponent().SetSecurityLevel(sec); return ExitCode.Normal; } diff --git a/TGCommandLine/DMCommands.cs b/TGCommandLine/DMCommands.cs index f734e731f4..4a61f9e13d 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 { @@ -27,15 +28,15 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var DM = Server.GetComponent(); + var DM = Interface.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 (Interface.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) @@ -83,29 +84,29 @@ namespace TGCommandLine void ShowError() { - var error = Server.GetComponent().CompileError(); + var error = Interface.GetComponent().CompileError(); if (error != null) OutputProc("Last error: " + error); } protected override ExitCode Run(IList parameters) { - var DM = Server.GetComponent(); + var DM = Interface.GetComponent(); OutputProc(String.Format("Target Project: /{0}.dme", DM.ProjectName())); 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; @@ -141,7 +142,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - Server.GetComponent().SetProjectName(parameters[0]); + Interface.GetComponent().SetProjectName(parameters[0]); return ExitCode.Normal; } } @@ -165,11 +166,11 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var DM = Server.GetComponent(); + var DM = Interface.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) @@ -205,7 +206,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var res = Server.GetComponent().Cancel(); + var res = Interface.GetComponent().Cancel(); OutputProc(res ?? "Success!"); return ExitCode.Normal; //because failing cancellation implys it's already cancelled } diff --git a/TGCommandLine/Program.cs b/TGCommandLine/Program.cs index 301da45f6c..1cc910a9cd 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; @@ -50,7 +51,7 @@ namespace TGCommandLine } argsAsList.RemoveAt(I); argsAsList.RemoveAt(I); - Server.SetRemoteLoginInformation(address, port, username, password); + Interface.SetRemoteLoginInformation(address, port, username, password); break; } } @@ -58,24 +59,24 @@ 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(); + var res = Interface.VerifyConnection(); if (res != null) { 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()) + if (!Interface.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)) + if (!SentVMMWarning && Interface.VersionMismatch(out string error)) { SentVMMWarning = true; Console.WriteLine(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() @@ -140,7 +141,7 @@ namespace TGCommandLine Command.OutputProcVar.Value = Console.WriteLine; if (args.Length != 0) { - Server.SetBadCertificateHandler((message) => { + Interface.SetBadCertificateHandler((message) => { foreach (var I in args) if (I.ToLower() == "--disable-ssl-verification") //im just not even going to document this because i hate it so much return true; @@ -150,7 +151,7 @@ namespace TGCommandLine return (int)RunCommandLine(new List(args)); } - Server.SetBadCertificateHandler(BadCertificateInteractive); + Interface.SetBadCertificateHandler(BadCertificateInteractive); Console.WriteLine("Type 'remote' to connect to a remote service"); //interactive mode @@ -178,22 +179,22 @@ namespace TGCommandLine var username = Console.ReadLine(); Console.Write("Enter password: "); var password = ReadLineSecure(); - Server.SetRemoteLoginInformation(address, port, username, password); - var res = Server.VerifyConnection(); + Interface.SetRemoteLoginInformation(address, port, username, password); + var res = Interface.VerifyConnection(); if (res != null) { Console.WriteLine("Unable to connect: " + res); - Server.MakeLocalConnection(); + Interface.MakeLocalConnection(); } - else if (!Server.Authenticate()) + else if (!Interface.Authenticate()) { Console.WriteLine("Authentication error: Username/password/windows identity is not authorized! Returning to local mode..."); - Server.MakeLocalConnection(); + Interface.MakeLocalConnection(); } else { Console.WriteLine("Connected remotely"); - if (Server.VersionMismatch(out res)) + if (Interface.VersionMismatch(out res)) { SentVMMWarning = true; Console.WriteLine(res); @@ -203,16 +204,16 @@ namespace TGCommandLine break; case "disconnect": SentVMMWarning = false; - Server.MakeLocalConnection(); + Interface.MakeLocalConnection(); Console.WriteLine("Switch to local mode"); 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; + Interface.GetComponent().PrepareForUpdate(); + return (int)Command.ExitCode.Normal; #endif default: //linq voodoo to get quoted strings diff --git a/TGCommandLine/RepoCommands.cs b/TGCommandLine/RepoCommands.cs index 1ad2101a4e..04d646e8f2 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 { @@ -31,7 +32,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var res = Server.GetComponent().UpdateTGS3Json(); + var res = Interface.GetComponent().UpdateTGS3Json(); if (res != null) { OutputProc(res); @@ -50,7 +51,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var res = Server.GetComponent().Setup(parameters[0], parameters.Count > 1 ? parameters[1] : "master"); + var res = Interface.GetComponent().Setup(parameters[0], parameters.Count > 1 ? parameters[1] : "master"); if (res != null) { OutputProc("Error: " + res); @@ -77,7 +78,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var Repo = Server.GetComponent(); + var Repo = Interface.GetComponent(); var busy = Repo.OperationInProgress(); if (!busy) { @@ -130,7 +131,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var result = Server.GetComponent().Reset(parameters.Count > 0 && parameters[0].ToLower() == "--origin"); + var result = Interface.GetComponent().Reset(parameters.Count > 0 && parameters[0].ToLower() == "--origin"); OutputProc(result ?? "Success!"); return result == null ? ExitCode.Normal : ExitCode.ServerError; } @@ -166,7 +167,7 @@ namespace TGCommandLine OutputProc("Invalid parameter: " + parameters[0]); return ExitCode.BadCommand; } - var res = Server.GetComponent().Update(hard); + var res = Interface.GetComponent().Update(hard); OutputProc(res ?? "Success"); return res == null ? ExitCode.Normal : ExitCode.ServerError; } @@ -187,7 +188,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var result = Server.GetComponent().GenerateChangelog(out string error); + var result = Interface.GetComponent().GenerateChangelog(out string error); OutputProc(error ?? "Success!"); if (result != null) OutputProc(result); @@ -207,7 +208,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var result = Server.GetComponent().PushChangelog(); + var result = Interface.GetComponent().SynchronizePush(); if(result != null) OutputProc(result); return result == null ? ExitCode.Normal : ExitCode.ServerError; @@ -227,7 +228,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - Server.GetComponent().SetCommitterEmail(parameters[0]); + Interface.GetComponent().SetCommitterEmail(parameters[0]); return ExitCode.Normal; } @@ -249,7 +250,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - Server.GetComponent().SetCommitterName(parameters[0]); + Interface.GetComponent().SetCommitterName(parameters[0]); return ExitCode.Normal; } public override string GetArgumentString() @@ -270,7 +271,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - Server.GetComponent().SetPythonPath(parameters[0]); + Interface.GetComponent().SetPythonPath(parameters[0]); return ExitCode.Normal; } public override string GetArgumentString() @@ -303,7 +304,7 @@ namespace TGCommandLine OutputProc("Invalid PR Number!"); return ExitCode.BadCommand; } - var res = Server.GetComponent().MergePullRequest(PR); + var res = Interface.GetComponent().MergePullRequest(PR); OutputProc(res ?? "Success"); return res == null ? ExitCode.Normal : ExitCode.ServerError; } @@ -330,7 +331,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var data = Server.GetComponent().MergedPullRequests(out string error); + var data = Interface.GetComponent().MergedPullRequests(out string error); if (data == null) { OutputProc(error); @@ -357,7 +358,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var data = Server.GetComponent().ListBackups(out string error); + var data = Interface.GetComponent().ListBackups(out string error); if (data == null) { OutputProc(error); @@ -384,7 +385,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var res = Server.GetComponent().Checkout(parameters[0]); + var res = Interface.GetComponent().Checkout(parameters[0]); OutputProc(res ?? "Success"); return res == null ? ExitCode.Normal : ExitCode.ServerError; } diff --git a/TGCommandLine/RootCommands.cs b/TGCommandLine/RootCommands.cs index 9fba17ca44..e78740e7e0 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 { @@ -9,7 +10,7 @@ namespace TGCommandLine public CLICommand() { var tmp = new List { new UpdateCommand(), new TestmergeCommand(), new RepoCommand(), new BYONDCommand(), new DMCommand(), new DDCommand(), new ConfigCommand(), new IRCCommand(), new DiscordCommand(), new AutoUpdateCommand(), new SetAutoUpdateCommand() }; - if (Server.VerifyConnection() == null && Server.Authenticate() && Server.AuthenticateAdmin()) + if (Interface.VerifyConnection() == null && Interface.Authenticate() && Interface.AuthenticateAdmin()) tmp.Add(new AdminCommand()); Children = tmp.ToArray(); } @@ -34,7 +35,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var res = Server.GetComponent().AutoUpdateInterval(); + var res = Interface.GetComponent().AutoUpdateInterval(); OutputProc(res == 0 ? "OFF" : String.Format("Auto updating every {0} minutes", res)); return ExitCode.Normal; } @@ -73,7 +74,7 @@ namespace TGCommandLine return ExitCode.BadCommand; } - Server.GetComponent().SetAutoUpdateInterval(NewInterval); + Interface.GetComponent().SetAutoUpdateInterval(NewInterval); return ExitCode.Normal; } @@ -88,7 +89,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { var gen_cl = parameters.Count > 1 && parameters[1].ToLower() == "--cl"; - var Repo = Server.GetComponent(); + var Repo = Interface.GetComponent(); switch (parameters[0].ToLower()) { case "hard": @@ -118,12 +119,12 @@ namespace TGCommandLine OutputProc(res); else { - res = Repo.PushChangelog(); + res = Repo.SynchronizePush(); if (res != null) OutputProc(res); } } - var resu = Server.GetComponent().Compile(true); + var resu = Interface.GetComponent().Compile(true); OutputProc(resu ? "Compilation started!" : "Compilation could not be started!"); return resu ? ExitCode.Normal : ExitCode.ServerError; } @@ -160,7 +161,7 @@ namespace TGCommandLine OutputProc("Invalid tesmerge #: " + parameters[0]); return ExitCode.BadCommand; } - var Repo = Server.GetComponent(); + var Repo = Interface.GetComponent(); var res = Repo.MergePullRequest(tm); if (res != null) { @@ -173,7 +174,7 @@ namespace TGCommandLine OutputProc(res); return ExitCode.ServerError; } - var resu = Server.GetComponent().Compile(true); + var resu = Interface.GetComponent().Compile(true); OutputProc(resu ? "Compilation started!" : "Compilation could not be started!"); return resu ? ExitCode.Normal : ExitCode.ServerError; } diff --git a/TGCommandLine/TGCommandLine.csproj b/TGCommandLine/TGCommandLine.csproj index cf32e3fcf0..528c4e11fd 100644 --- a/TGCommandLine/TGCommandLine.csproj +++ b/TGCommandLine/TGCommandLine.csproj @@ -12,32 +12,34 @@ 512 true - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - true - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - tgs.ico + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + true + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + true + true + bin\x86\Release\TGCommandLine.xml + diff --git a/TGControlPanel/ByondPage.cs b/TGControlPanel/ByondPage.cs index 62fccce395..7f120c3705 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 { @@ -9,10 +10,10 @@ namespace TGControlPanel string lastReadError = null; void InitBYONDPage() { - var BYOND = Server.GetComponent(); - var CV = BYOND.GetVersion(TGByondVersion.Installed); + var BYOND = Interface.GetComponent(); + 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 = Interface.GetComponent().GetVersion(ByondVersion.Latest); LatestVersionLabel.Text = latestVer; try @@ -46,7 +47,7 @@ namespace TGControlPanel private void UpdateButton_Click(object sender, EventArgs e) { UpdateBYONDButtons(); - if (!Server.GetComponent().UpdateToVersion((int)MajorVersionNumeric.Value, (int)MinorVersionNumeric.Value)) + if (!Interface.GetComponent().UpdateToVersion((int)MajorVersionNumeric.Value, (int)MinorVersionNumeric.Value)) MessageBox.Show("Unable to begin update, there is another operation in progress."); } @@ -56,40 +57,40 @@ namespace TGControlPanel } void UpdateBYONDButtons() { - var BYOND = Server.GetComponent(); + var BYOND = Interface.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; } - var error = Server.GetComponent().GetError(); + var error = Interface.GetComponent().GetError(); if (error != lastReadError) { lastReadError = error; diff --git a/TGControlPanel/ChatPage.cs b/TGControlPanel/ChatPage.cs index 74eee5244d..1bf6460e54 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,23 +10,23 @@ 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; } } void LoadChatPage() { updatingChat = true; - var Chat = Server.GetComponent(); + var Chat = Interface.GetComponent(); var PI = Chat.ProviderInfos()[(int)ModifyingProvider]; ChatAdminsTextBox.Visible = true; 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; } @@ -111,7 +112,7 @@ namespace TGControlPanel private void ChatReconnectButton_Click(object sender, EventArgs e) { - Server.GetComponent().Reconnect(ModifyingProvider); + Interface.GetComponent().Reconnect(ModifyingProvider); LoadChatPage(); } @@ -133,7 +134,7 @@ namespace TGControlPanel { if (!updatingChat && DiscordProviderSwitch.Checked) { - ModifyingProvider = TGChatProvider.Discord; + ModifyingProvider = ChatProvider.Discord; LoadChatPage(); } } @@ -142,13 +143,13 @@ namespace TGControlPanel { if (!updatingChat && IRCProviderSwitch.Checked) { - ModifyingProvider = TGChatProvider.IRC; + ModifyingProvider = ChatProvider.IRC; LoadChatPage(); } } void SetAdminsAreSpecial(bool value) { - var Chat = Server.GetComponent(); + var Chat = Interface.GetComponent(); var PI = Chat.ProviderInfos()[(int)ModifyingProvider]; PI.AdminsAreSpecial = value; var res = Chat.SetProviderInfo(PI); @@ -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, @@ -207,7 +208,7 @@ namespace TGControlPanel wip.Enabled = ChatEnabledCheckbox.Checked; wip.AdminsAreSpecial = AdminModeSpecial.Checked; - res = Server.GetComponent().SetProviderInfo(wip); + res = Interface.GetComponent().SetProviderInfo(wip); } if (res != null) MessageBox.Show(res); diff --git a/TGControlPanel/Login.cs b/TGControlPanel/Login.cs index 557d484924..8c4b28437f 100644 --- a/TGControlPanel/Login.cs +++ b/TGControlPanel/Login.cs @@ -4,8 +4,11 @@ using TGServiceInterface; namespace TGControlPanel { - public partial class Login : Form + partial class Login : Form { + /// + /// Create a form + /// public Login() { InitializeComponent(); @@ -28,7 +31,7 @@ namespace TGControlPanel { IPTextBox.Text = IPTextBox.Text.Trim(); UsernameTextBox.Text = UsernameTextBox.Text.Trim(); - Server.SetRemoteLoginInformation(IPTextBox.Text, (ushort)PortSelector.Value, UsernameTextBox.Text, PasswordTextBox.Text); + Interface.SetRemoteLoginInformation(IPTextBox.Text, (ushort)PortSelector.Value, UsernameTextBox.Text, PasswordTextBox.Text); var Config = Properties.Settings.Default; Config.RemoteIP = IPTextBox.Text; Config.RemoteUsername = UsernameTextBox.Text; @@ -48,20 +51,20 @@ namespace TGControlPanel private void LocalLoginButton_Click(object sender, EventArgs e) { - Server.MakeLocalConnection(); + Interface.MakeLocalConnection(); Properties.Settings.Default.RemoteDefault = false; VerifyAndConnect(); } void VerifyAndConnect() { - var res = Server.VerifyConnection(); + var res = Interface.VerifyConnection(); if (res != null) { MessageBox.Show("Unable to connect to service! Error: " + res); return; } - if (!Server.Authenticate()) + if (!Interface.Authenticate()) { MessageBox.Show("Authentication error: Username/password/windows identity is not authorized! Ensure you are a system administrator or in the correct Windows group on the service machine."); return; diff --git a/TGControlPanel/Main.cs b/TGControlPanel/Main.cs index 969e926771..4c1e9af2e7 100644 --- a/TGControlPanel/Main.cs +++ b/TGControlPanel/Main.cs @@ -5,12 +5,18 @@ using TGServiceInterface; namespace TGControlPanel { - public partial class Main : Form + /// + /// The main form + /// + partial class Main : Form { + /// + /// Create the control panel. Requires the has had it's connection info setup + /// public Main() { InitializeComponent(); - if (Server.VersionMismatch(out string error) && MessageBox.Show(error, "Warning", MessageBoxButtons.OKCancel) == DialogResult.Cancel) + if (Interface.VersionMismatch(out string error) && MessageBox.Show(error, "Warning", MessageBoxButtons.OKCancel) == DialogResult.Cancel) { Close(); return; diff --git a/TGControlPanel/Program.cs b/TGControlPanel/Program.cs index d5c1ee1988..b4eec9afc3 100644 --- a/TGControlPanel/Program.cs +++ b/TGControlPanel/Program.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.InteropServices; using System.Windows.Forms; using TGServiceInterface; @@ -10,7 +9,7 @@ namespace TGControlPanel [STAThread] static void Main(string[] args) { - Server.SetBadCertificateHandler(BadCertificateHandler); + Interface.SetBadCertificateHandler(BadCertificateHandler); try { if (Properties.Settings.Default.UpgradeRequired) @@ -47,7 +46,7 @@ namespace TGControlPanel public static bool CheckAdminWithWarning() { - if (!Server.AuthenticateAdmin()) + if (!Interface.AuthenticateAdmin()) { MessageBox.Show("Only system administrators may use this command!"); return false; diff --git a/TGControlPanel/RepoPage.cs b/TGControlPanel/RepoPage.cs index 15983de0ec..076933a6b6 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 { @@ -76,7 +77,7 @@ namespace TGControlPanel if (RepoBusyCheck()) return; - var Repo = Server.GetComponent(); + var Repo = Interface.GetComponent(); RepoProgressBar.Style = ProgressBarStyle.Marquee; RepoProgressBar.Visible = false; @@ -149,7 +150,7 @@ namespace TGControlPanel bool RepoBusyCheck() { - if (Server.GetComponent().OperationInProgress()) + if (Interface.GetComponent().OperationInProgress()) { DoAsyncOp(RepoAction.Wait, "Waiting for repository to finish another action..."); return true; @@ -171,7 +172,7 @@ namespace TGControlPanel private void RepoBGW_DoWork(object sender, DoWorkEventArgs e) { //Only for clones - var Repo = Server.GetComponent(); + var Repo = Interface.GetComponent(); switch (action) { case RepoAction.Clone: @@ -212,7 +213,7 @@ namespace TGControlPanel } void UpdatePythonPath() { - if (!Server.GetComponent().SetPythonPath(PythonPathText.Text)) + if (!Interface.GetComponent().SetPythonPath(PythonPathText.Text)) MessageBox.Show("Python could not be found in the selected location!"); } private void CloneRepositoryButton_Click(object sender, EventArgs e) @@ -280,7 +281,7 @@ namespace TGControlPanel } private void RepoApplyButton_Click(object sender, EventArgs e) { - var Repo = Server.GetComponent(); + var Repo = Interface.GetComponent(); if (RepoBusyCheck()) return; @@ -352,7 +353,7 @@ namespace TGControlPanel { if (MessageBox.Show("This will update the cached TGS3.json to the current repository version, potentially redefining symlinks. Proceed?", "Json Update", MessageBoxButtons.YesNo) != DialogResult.Yes) return; - var res = Server.GetComponent().UpdateTGS3Json(); + var res = Interface.GetComponent().UpdateTGS3Json(); if (res != null) MessageBox.Show(res); } diff --git a/TGControlPanel/ServerPage.cs b/TGControlPanel/ServerPage.cs index 0f001d3abc..75f7b63ce4 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 { @@ -24,7 +25,7 @@ namespace TGControlPanel void InitServerPage() { LoadServerPage(); - if (!Server.AuthenticateAdmin()) + if (!Interface.AuthenticateAdmin()) { ServerPathTextbox.Enabled = false; ServerPathTextbox.ReadOnly = true; @@ -69,7 +70,7 @@ namespace TGControlPanel private void CompileCancelButton_Click(object sender, EventArgs e) { - var res = Server.GetComponent().Cancel(); + var res = Interface.GetComponent().Cancel(); if (res != null) MessageBox.Show(res); LoadServerPage(); @@ -88,7 +89,7 @@ namespace TGControlPanel ServerPathTextbox.ReadOnly = true; return; } - if (updatingFields || ServerPathTextbox.Text.Trim() == Server.GetComponent().ServerDirectory()) + if (updatingFields || ServerPathTextbox.Text.Trim() == Interface.GetComponent().ServerDirectory()) return; var DialogResult = MessageBox.Show("This will move the entire server installation.", "Confim", MessageBoxButtons.YesNo); if (DialogResult != DialogResult.Yes) @@ -100,12 +101,12 @@ namespace TGControlPanel ServerPathTextbox.ReadOnly = true; return; } - MessageBox.Show(Server.GetComponent().MoveServer(ServerPathTextbox.Text) ?? "Success!"); + MessageBox.Show(Interface.GetComponent().MoveServer(ServerPathTextbox.Text) ?? "Success!"); } void LoadServerPage() { - var RepoExists = Server.GetComponent().Exists(); + var RepoExists = Interface.GetComponent().Exists(); compileButton.Visible = RepoExists; AutoUpdateCheckbox.Visible = RepoExists; initializeButton.Visible = RepoExists; @@ -137,10 +138,10 @@ namespace TGControlPanel if (updatingFields) return; - var DM = Server.GetComponent(); - var DD = Server.GetComponent(); - var Config = Server.GetComponent(); - var Repo = Server.GetComponent(); + var DM = Interface.GetComponent(); + var DD = Interface.GetComponent(); + var Config = Interface.GetComponent(); + var Repo = Interface.GetComponent(); try { @@ -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; @@ -246,13 +247,13 @@ namespace TGControlPanel void UpdateProjectName() { if (!updatingFields) - Server.GetComponent().SetProjectName(projectNameText.Text); + Interface.GetComponent().SetProjectName(projectNameText.Text); } private void PortSelector_ValueChanged(object sender, EventArgs e) { if (!updatingFields) - Server.GetComponent().SetPort((ushort)PortSelector.Value); + Interface.GetComponent().SetPort((ushort)PortSelector.Value); } private void RunServerUpdate(FullUpdateAction fua, ushort tm = 0) @@ -292,13 +293,13 @@ namespace TGControlPanel private void InitializeButton_Click(object sender, EventArgs e) { - if (!Server.GetComponent().Initialize()) + if (!Interface.GetComponent().Initialize()) MessageBox.Show("Unable to start initialization!"); LoadServerPage(); } private void CompileButton_Click(object sender, EventArgs e) { - if (!Server.GetComponent().Compile()) + if (!Interface.GetComponent().Compile()) MessageBox.Show("Unable to start compilation!"); LoadServerPage(); } @@ -306,7 +307,7 @@ namespace TGControlPanel private void AutostartCheckbox_CheckedChanged(object sender, System.EventArgs e) { if (!updatingFields) - Server.GetComponent().SetAutostart(AutostartCheckbox.Checked); + Interface.GetComponent().SetAutostart(AutostartCheckbox.Checked); } private void ServerStartButton_Click(object sender, System.EventArgs e) { @@ -318,7 +319,7 @@ namespace TGControlPanel { try { - e.Result = Server.GetComponent().Start(); + e.Result = Interface.GetComponent().Start(); } catch (Exception ex) { @@ -331,7 +332,7 @@ namespace TGControlPanel var DialogResult = MessageBox.Show("This will immediately shut down the server. Continue?", "Confim", MessageBoxButtons.YesNo); if (DialogResult == DialogResult.No) return; - var res = Server.GetComponent().Stop(); + var res = Interface.GetComponent().Stop(); if (res != null) MessageBox.Show(res); } @@ -341,7 +342,7 @@ namespace TGControlPanel var DialogResult = MessageBox.Show("This will immediately restart the server. Continue?", "Confim", MessageBoxButtons.YesNo); if (DialogResult == DialogResult.No) return; - var res = Server.GetComponent().Restart(); + var res = Interface.GetComponent().Restart(); if (res != null) MessageBox.Show(res); } @@ -353,7 +354,7 @@ namespace TGControlPanel var DialogResult = MessageBox.Show("This will shut down the server when the current round ends. Continue?", "Confim", MessageBoxButtons.YesNo); if (DialogResult == DialogResult.No) return; - Server.GetComponent().RequestStop(); + Interface.GetComponent().RequestStop(); LoadServerPage(); } @@ -362,7 +363,7 @@ namespace TGControlPanel var DialogResult = MessageBox.Show("This will restart the server when the current round ends. Continue?", "Confim", MessageBoxButtons.YesNo); if (DialogResult == DialogResult.No) return; - Server.GetComponent().RequestRestart(); + Interface.GetComponent().RequestRestart(); } @@ -370,8 +371,8 @@ namespace TGControlPanel { try { - var Repo = Server.GetComponent(); - var DM = Server.GetComponent(); + var Repo = Interface.GetComponent(); + var DM = Interface.GetComponent(); switch (fuAction) { case FullUpdateAction.Testmerge: @@ -388,7 +389,7 @@ namespace TGControlPanel { Repo.GenerateChangelog(out updateError); if (updateError == null) - updateError = Repo.PushChangelog(); + updateError = Repo.SynchronizePush(); updateError = DM.Compile(true) ? updateError : "Compilation failed!"; } break; @@ -398,7 +399,7 @@ namespace TGControlPanel { Repo.GenerateChangelog(out updateError); if (updateError == null) - updateError = Repo.PushChangelog(); + updateError = Repo.SynchronizePush(); updateError = Repo.MergePullRequest(testmergePR); if (updateError == null) { @@ -413,7 +414,7 @@ namespace TGControlPanel { Repo.GenerateChangelog(out updateError); if (updateError == null) - Repo.PushChangelog(); //not an error 99% of the time if this fails, just a dirty tree + Repo.SynchronizePush(); //not an error 99% of the time if this fails, just a dirty tree updateError = DM.Compile(true) ? updateError : "Compilation failed!"; } break; @@ -459,7 +460,7 @@ namespace TGControlPanel private void SecuritySelector_SelectedIndexChanged(object sender, EventArgs e) { if (!updatingFields) - if (!Server.GetComponent().SetSecurityLevel((TGDreamDaemonSecurity)SecuritySelector.SelectedIndex)) + if (!Interface.GetComponent().SetSecurityLevel((DreamDaemonSecurity)SecuritySelector.SelectedIndex)) MessageBox.Show("Security change will be applied after next server reboot."); } @@ -468,7 +469,7 @@ namespace TGControlPanel var msg = WorldAnnounceField.Text; if (!String.IsNullOrWhiteSpace(msg)) { - var res = Server.GetComponent().WorldAnnounce(msg); + var res = Interface.GetComponent().WorldAnnounce(msg); if (res != null) { MessageBox.Show(res); @@ -481,13 +482,13 @@ namespace TGControlPanel private void WebclientCheckBox_CheckedChanged(object sender, EventArgs e) { if (!updatingFields) - Server.GetComponent().SetWebclient(WebclientCheckBox.Checked); + Interface.GetComponent().SetWebclient(WebclientCheckBox.Checked); } private void AutoUpdateInterval_ValueChanged(object sender, EventArgs e) { if (!updatingFields) - Server.GetComponent().SetAutoUpdateInterval((ulong)AutoUpdateInterval.Value); + Interface.GetComponent().SetAutoUpdateInterval((ulong)AutoUpdateInterval.Value); } private void AutoUpdateCheckbox_CheckedChanged(object sender, EventArgs e) @@ -498,9 +499,9 @@ namespace TGControlPanel AutoUpdateInterval.Visible = on; AutoUpdateMLabel.Visible = on; if (!on) - Server.GetComponent().SetAutoUpdateInterval(0); + Interface.GetComponent().SetAutoUpdateInterval(0); else - Server.GetComponent().SetAutoUpdateInterval((ulong)AutoUpdateInterval.Value); + Interface.GetComponent().SetAutoUpdateInterval((ulong)AutoUpdateInterval.Value); } } } diff --git a/TGControlPanel/StaticPage.cs b/TGControlPanel/StaticPage.cs index 3f914afabc..e04eb02c38 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 { @@ -22,7 +23,7 @@ namespace TGControlPanel void InitStaticPage() { - if(!Server.AuthenticateAdmin()) + if(!Interface.AuthenticateAdmin()) RecreateStaticButton.Visible = false; BuildFileList(); } @@ -33,7 +34,7 @@ namespace TGControlPanel IndexesToPaths.Clear(); StaticFileListBox.Items.Clear(); IndexesToPaths.Add(StaticFileListBox.Items.Add("/"), "/"); - if (EnumeratePath("", Server.GetComponent(), 1) == EnumResult.Unauthorized) + if (EnumeratePath("", Interface.GetComponent(), 1) == EnumResult.Unauthorized) { StaticFileListBox.Items[0] += " (UNAUTHORIZED)"; IndexesToPaths[0] = null; @@ -139,7 +140,7 @@ namespace TGControlPanel if (error == null) try { - error = Server.GetComponent().WriteText(FileName, fileContents, out bool unauthorized); + error = Interface.GetComponent().WriteText(FileName, fileContents, out bool unauthorized); } catch (Exception ex) { @@ -163,7 +164,7 @@ namespace TGControlPanel string text, error; try { - text = Server.GetComponent().ReadText(remotePath, false, out error, out bool unauthorized); + text = Interface.GetComponent().ReadText(remotePath, false, out error, out bool unauthorized); } catch (Exception ex) { @@ -205,7 +206,7 @@ namespace TGControlPanel { if (MessageBox.Show("Are you sure you want to delete " + ((string)StaticFileListBox.SelectedItem).Trim() + "?", "Confirm", MessageBoxButtons.YesNo) != DialogResult.Yes) return; - var res = Server.GetComponent().DeleteFile(IndexesToPaths[StaticFileListBox.SelectedIndex], out bool unauthorized); + var res = Interface.GetComponent().DeleteFile(IndexesToPaths[StaticFileListBox.SelectedIndex], out bool unauthorized); if (res != null) MessageBox.Show(res); BuildFileList(); @@ -228,7 +229,7 @@ namespace TGControlPanel var FullFileName = Path.Combine(IndexesToPaths[StaticFileListBox.SelectedIndex], FileName); if (resu == DialogResult.Yes) FullFileName = Path.Combine(FullFileName, "__TGS3_CP_DIRECTORY_CREATOR__"); - var config = Server.GetComponent(); + var config = Interface.GetComponent(); var res = config.WriteText(FullFileName, "", out bool unauthorized); if (res != null) MessageBox.Show(res); @@ -247,7 +248,7 @@ namespace TGControlPanel bool unauthorized; try { - res = Server.GetComponent().WriteText(IndexesToPaths[index], StaticFileEditTextbox.Text, out unauthorized); + res = Interface.GetComponent().WriteText(IndexesToPaths[index], StaticFileEditTextbox.Text, out unauthorized); } catch (Exception ex) { @@ -299,7 +300,7 @@ namespace TGControlPanel bool unauthorized; try { - entry = Server.GetComponent().ReadText(path, false, out error, out unauthorized); + entry = Interface.GetComponent().ReadText(path, false, out error, out unauthorized); } catch(Exception e) { @@ -336,7 +337,7 @@ namespace TGControlPanel RecreateStaticButton.Visible = false; return; } - var res = Server.GetComponent().RecreateStaticFolder(); + var res = Interface.GetComponent().RecreateStaticFolder(); if (res != null) MessageBox.Show(res); BuildFileList(); diff --git a/TGControlPanel/TGControlPanel.csproj b/TGControlPanel/TGControlPanel.csproj index 976c556e7d..87c59e1792 100644 --- a/TGControlPanel/TGControlPanel.csproj +++ b/TGControlPanel/TGControlPanel.csproj @@ -14,29 +14,31 @@ - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - true - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - tgs.ico + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + true + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + true + true + bin\x86\Release\TGControlPanel.xml + diff --git a/TGInstallerWrapper/FodyWeavers.xml b/TGInstallerWrapper/FodyWeavers.xml new file mode 100644 index 0000000000..c6e1b7c8af --- /dev/null +++ b/TGInstallerWrapper/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/TGInstallerWrapper/Main.Designer.cs b/TGInstallerWrapper/Main.Designer.cs index 42842a7c05..88b82a06a3 100644 --- a/TGInstallerWrapper/Main.Designer.cs +++ b/TGInstallerWrapper/Main.Designer.cs @@ -1,4 +1,8 @@ -namespace TGInstallerWrapper +using System; +using System.Diagnostics; +using System.Reflection; + +namespace TGInstallerWrapper { partial class Main { @@ -127,7 +131,7 @@ this.VersionLabel.Name = "VersionLabel"; this.VersionLabel.Size = new System.Drawing.Size(228, 18); this.VersionLabel.TabIndex = 8; - this.VersionLabel.Text = "None (No Service Running)"; + this.VersionLabel.Text = "Unknown (No service running)"; // // InstallButton // @@ -160,7 +164,7 @@ this.TargetVersionLabel.Name = "TargetVersionLabel"; this.TargetVersionLabel.Size = new System.Drawing.Size(132, 18); this.TargetVersionLabel.TabIndex = 11; - this.TargetVersionLabel.Text = "Target Version:"; + this.TargetVersionLabel.Text = "Target Version: v" + FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion; // // InstallCancelButton // @@ -179,6 +183,7 @@ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(39)))), ((int)(((byte)(40)))), ((int)(((byte)(34))))); this.ClientSize = new System.Drawing.Size(537, 153); + this.FormClosing += Main_FormClosing; this.Controls.Add(this.InstallCancelButton); this.Controls.Add(this.TargetVersionLabel); this.Controls.Add(this.ShowLogCheckbox); diff --git a/TGInstallerWrapper/Main.cs b/TGInstallerWrapper/Main.cs index 43045bb0cc..79d8ac3081 100644 --- a/TGInstallerWrapper/Main.cs +++ b/TGInstallerWrapper/Main.cs @@ -3,36 +3,126 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -using TGServiceInterface; namespace TGInstallerWrapper { - public partial class Main : Form + partial class Main : Form { - const string InstallDir = "TG Station Server"; //keep this in sync with the msi installer + const string DefaultInstallDir = "TG Station Server"; //keep this in sync with the msi installer + + //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 = InterfaceComponentsNamespace + ".ITGSService"; //fuck this typo + const string InterfaceClassVerifyConnection = "VerifyConnection"; + const string InterfaceClassGetComponent = "GetComponent"; + const string InterfaceServiceInterfaceVersion = "Version"; + const string InterfaceServiceInterfacePrepareForUpdate = "PrepareForUpdate"; + + Assembly InterfaceAssembly; + Type Server, ITGSService; + MethodInfo VerifyConnection, GetComponentITGSService, Version, PrepareForUpdate; + + string tempDir; bool installing = false; bool cancelled = false; bool pathIsDefault = true; + + /// + /// Construct an installer form + /// public Main() { InitializeComponent(); - FormClosing += Main_FormClosing; - PathTextBox.Text = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86) + Path.DirectorySeparatorChar + InstallDir; - var verifiedConnection = Server.VerifyConnection() == null; + SetupTempDir(); + LoadInterfaceFromReflection(); + CheckForExistingVersion(); + PathTextBox.Text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), DefaultInstallDir); + } + + void SetupTempDir() + { + tempDir = Path.Combine(Path.GetTempPath(), "TGS3InstallerTempDir"); try { - VersionLabel.Text = Server.GetComponent().Version(); + if (File.Exists(tempDir)) + File.Delete(tempDir); + else if (Directory.Exists(tempDir)) + Directory.Delete(tempDir); + } + catch { } + if (File.Exists(tempDir) || Directory.Exists(tempDir)) + { + tempDir = Path.GetTempFileName(); + File.Delete(tempDir); //we want a dir not a file + } + Directory.CreateDirectory(tempDir); + } + + void LoadInterfaceFromReflection() + { + //so this is where we expect to find the interface dll + try + { + var tmppath = Path.Combine(tempDir, InterfaceDLL); + File.WriteAllBytes(tmppath, Properties.Resources.TGServiceInterface); + InterfaceAssembly = Assembly.LoadFrom(tmppath); //we can't link to it, or load the bytes directly because the thing will complain about mixing the DLLExport code and IL code + Server = InterfaceAssembly.GetType(InterfaceClass); + ITGSService = InterfaceAssembly.GetType(InterfaceServiceInterface); + VerifyConnection = Server.GetMethod(InterfaceClassVerifyConnection); + GetComponentITGSService = Server.GetMethod(InterfaceClassGetComponent).MakeGenericMethod(ITGSService); + Version = ITGSService.GetMethod(InterfaceServiceInterfaceVersion); + PrepareForUpdate = ITGSService.GetMethod(InterfaceServiceInterfacePrepareForUpdate); } catch { - if(verifiedConnection) + InterfaceAssembly = null; + VersionLabel.Text = "Error: (Could not load interface dll)"; + return; + } + } + + void CheckForExistingVersion() { + if (InterfaceAssembly == null) + return; + var verifiedConnection = VerifyConnection.Invoke(null, null) == null; + try + { + VersionLabel.Text = (string)Version.Invoke(GetComponentITGSService.Invoke(null, null), null); + } + catch + { + if (verifiedConnection) VersionLabel.Text = "< v3.0.85.0 (Missing ITGService.Version())"; } - TargetVersionLabel.Text += " v" + FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).FileVersion; + } + + bool ConfirmDangerousUpgrade() + { + return MessageBox.Show("Unable connect to service! Existing DreamDaemon instances will be terminated. Continue?", "Warning", MessageBoxButtons.YesNo) == DialogResult.Yes; + } + + bool TellServiceWereComingForThem() + { + if (InterfaceAssembly == null) + return ConfirmDangerousUpgrade(); + var connectionVerified = VerifyConnection.Invoke(null, null) == null; + try + { + PrepareForUpdate.Invoke(GetComponentITGSService.Invoke(null, null), null); + Thread.Sleep(3000); //chat messages + return true; + } + catch + { + return ConfirmDangerousUpgrade(); + } } private void Main_FormClosing(object sender, FormClosingEventArgs e) @@ -71,7 +161,6 @@ namespace TGInstallerWrapper async void DoInstall() { - string path = null; string logfile = null; try { @@ -90,6 +179,9 @@ namespace TGInstallerWrapper break; } + if (!TellServiceWereComingForThem()) + return; + var args = new List(); if (!pathIsDefault) args.Add(String.Format("INSTALLFOLDER=\"{0}\"", PathTextBox.Text)); @@ -108,32 +200,16 @@ namespace TGInstallerWrapper ShowLogCheckbox.Enabled = false; InstallButton.Text = "Installing..."; - path = Path.GetTempFileName(); - File.Delete(path); //we want a dir not a file - Directory.CreateDirectory(path); - var msipath = path + Path.DirectorySeparatorChar + "TGServiceInstaller.msi"; + var msipath = Path.Combine(tempDir, "TGServiceInstaller.msi"); File.WriteAllBytes(msipath, Properties.Resources.TGServiceInstaller); - File.WriteAllBytes(path + Path.DirectorySeparatorChar + "cab1.cab", Properties.Resources.cab1); + File.WriteAllBytes(Path.Combine(tempDir, "cab1.cab"), Properties.Resources.cab1); ProgressBar.Style = ProgressBarStyle.Marquee; - - var connectionVerified = Server.VerifyConnection() == null; - try - { - Server.GetComponent().PrepareForUpdate(); - Thread.Sleep(3000); //chat messages - } - catch - { - if (connectionVerified && MessageBox.Show("ITGSService.PrepareForUpdate() threw an exception! Existing DreamDaemon instances will be terminated. Continue?", "Warning", MessageBoxButtons.YesNo) != DialogResult.Yes) - return; - } - InstallCancelButton.Enabled = true; if (ShowLogCheckbox.Checked) { - logfile = path + Path.DirectorySeparatorChar + "tgsinstall.log"; + logfile = Path.Combine(tempDir, "tgsinstall.log"); Installer.EnableLog(InstallLogModes.Verbose | InstallLogModes.PropertyDump, logfile); } var cl = String.Join(" ", args); @@ -172,12 +248,6 @@ namespace TGInstallerWrapper Process.Start(logfile).WaitForInputIdle(); } catch { } - if (path != null) - try - { - Directory.Delete(path, true); - } - catch { } } MessageBox.Show("Success!"); Application.Exit(); @@ -208,7 +278,7 @@ namespace TGInstallerWrapper if (fbd.ShowDialog() != DialogResult.OK) return; pathIsDefault = false; - PathTextBox.Text = fbd.SelectedPath + Path.DirectorySeparatorChar + InstallDir; + PathTextBox.Text = fbd.SelectedPath + Path.DirectorySeparatorChar + DefaultInstallDir; } private void CancelButton_Click(object sender, EventArgs e) diff --git a/TGInstallerWrapper/Program.cs b/TGInstallerWrapper/Program.cs index c9669327d9..96dc421f86 100644 --- a/TGInstallerWrapper/Program.cs +++ b/TGInstallerWrapper/Program.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using System.Windows.Forms; namespace TGInstallerWrapper diff --git a/TGInstallerWrapper/Properties/Resources.Designer.cs b/TGInstallerWrapper/Properties/Resources.Designer.cs index 9365443396..e3858daec9 100644 --- a/TGInstallerWrapper/Properties/Resources.Designer.cs +++ b/TGInstallerWrapper/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace TGInstallerWrapper.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -79,5 +79,15 @@ namespace TGInstallerWrapper.Properties { return ((byte[])(obj)); } } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] TGServiceInterface { + get { + object obj = ResourceManager.GetObject("TGServiceInterface", resourceCulture); + return ((byte[])(obj)); + } + } } } diff --git a/TGInstallerWrapper/Properties/Resources.resx b/TGInstallerWrapper/Properties/Resources.resx index b29efcb08f..daa2b3cb0c 100644 --- a/TGInstallerWrapper/Properties/Resources.resx +++ b/TGInstallerWrapper/Properties/Resources.resx @@ -122,6 +122,9 @@ ..\..\TGServiceInstaller\bin\Release\cab1.cab;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - ...\..\TGServiceInstaller\bin\Release\TGServiceInstaller.msi;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ..\..\TGServiceInstaller\bin\Release\TGServiceInstaller.msi;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\..\TGServiceInterface\bin\x86\Release\TGServiceInterface.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 \ No newline at end of file diff --git a/TGInstallerWrapper/TGInstallerWrapper.csproj b/TGInstallerWrapper/TGInstallerWrapper.csproj index e94ed50a6c..7b9fa7692a 100644 --- a/TGInstallerWrapper/TGInstallerWrapper.csproj +++ b/TGInstallerWrapper/TGInstallerWrapper.csproj @@ -11,25 +11,8 @@ v4.5.2 512 true - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 + + app.manifest @@ -37,7 +20,33 @@ tgs.ico + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + true + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + true + true + bin\x86\Release\TG Station Server Installer.xml + + + ..\packages\Costura.Fody.1.6.2\lib\dotnet\Costura.dll + False + @@ -70,15 +79,20 @@ + - - {ac4e7e8b-f83a-481c-a8b0-8fa4e8ae59ab} - TGServiceInterface - - - + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/TGInstallerWrapper/packages.config b/TGInstallerWrapper/packages.config new file mode 100644 index 0000000000..b4d46b584a --- /dev/null +++ b/TGInstallerWrapper/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/TGS3Release.ps1 b/TGS3Release.ps1 deleted file mode 100644 index 0749e410db..0000000000 --- a/TGS3Release.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -$bf = $Env:APPVEYOR_BUILD_FOLDER -$src = "$bf\TGInstallerWrapper\bin\Release" -$version = [System.Diagnostics.FileVersionInfo]::GetVersionInfo("$src\TG Station Server Installer.exe").FileVersion - -Remove-Item "$src\Microsoft.Deployment.WindowsInstaller.xml" -Remove-Item "$src\TG Station Server Installer.exe.config" -Remove-Item "$src\TG Station Server Installer.pdb" -Rename-Item -Path "$src\TG Station Server Installer.exe" -NewName "$src\TG Station Server Installer v$version.exe" - -$destination = "$bf\TGS3-Server-v$version.zip" - -If(Test-path $destination) {Remove-item $destination} - -Add-Type -assembly "system.io.compression.filesystem" - -[io.compression.zipfile]::CreateFromDirectory($src, $destination) - -$destination_md5sha = $Env:APPVEYOR_BUILD_FOLDER + "\MD5-SHA1-Server-v$version.txt" - -$src2 = $Env:APPVEYOR_BUILD_FOLDER + "\ClientApps" -[system.io.directory]::CreateDirectory($src2) -Copy-Item "$bf\TGCommandLine\bin\Release\TGCommandLine.exe" "$src2\TGCommandLine.exe" -Copy-Item "$bf\TGControlPanel\bin\Release\TGControlPanel.exe" "$src2\TGControlPanel.exe" -Copy-Item "$bf\TGServiceInterface\bin\x86\Release\TGServiceInterface.dll" "$src2\TGServiceInterface.dll" - -$dest2 = "$bf\TGS3-Client-v$version.zip" - -[io.compression.zipfile]::CreateFromDirectory($src2, $dest2) -$destination_md5sha2 = $Env:APPVEYOR_BUILD_FOLDER + "\MD5-SHA1-Client-v$version.txt" - -& fciv -both $destination > $destination_md5sha -& fciv -both $dest2 > $destination_md5sha2 diff --git a/TGServerService/AdministrativeAuthorizationManager.cs b/TGServerService/AdministrativeAuthorizationManager.cs index 94b6734de7..8390f64788 100644 --- a/TGServerService/AdministrativeAuthorizationManager.cs +++ b/TGServerService/AdministrativeAuthorizationManager.cs @@ -1,9 +1,12 @@ using System.Security.Principal; using System.ServiceModel; -using TGServiceInterface; +using TGServiceInterface.Components; namespace TGServerService { + /// + /// A used to determine only if the caller is an admin + /// class AdministrativeAuthorizationManager : ServiceAuthorizationManager { string LastSeenUser; @@ -24,7 +27,7 @@ namespace TGServerService if (LastSeenUser != user) { LastSeenUser = user; - TGServerService.WriteAccess(user, authSuccess); + Service.WriteAccess(user, authSuccess); } return authSuccess; } diff --git a/TGServerService/App.config b/TGServerService/App.config index 7e46f8017d..42fede526f 100644 --- a/TGServerService/App.config +++ b/TGServerService/App.config @@ -10,68 +10,6 @@ - - - tgstation - - - 1337 - - - False - - - tgstation-server - - - tgstation-server@tgstation13.org - - - 0 - - - False - - - NEEDS INITIALIZING - - - - - - True - - - True - - - 0 - - - 0 - - - - - - False - - - - - - - - - 0 - - - 7 - - - TGStation - - C:\Python27 @@ -85,9 +23,6 @@ 38607 - - False - diff --git a/TGServerService/Byond.cs b/TGServerService/Byond.cs deleted file mode 100644 index e8c4646fab..0000000000 --- a/TGServerService/Byond.cs +++ /dev/null @@ -1,296 +0,0 @@ -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; - -namespace TGServerService -{ - partial class TGStationServer : ITGByond - { - const string ByondDirectory = "BYOND"; - const string StagingDirectory = "BYOND_staged"; - const string StagingDirectoryInner = "BYOND_staged/byond"; - const string RevisionDownloadPath = "BYONDRevision.zip"; - const string VersionFile = "/byond_version.dat"; - const string ByondRevisionsURL = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip"; - const string ByondLatestURL = "https://secure.byond.com/download/build/LATEST/"; - - const string ByondConfigDir = "BYOND_staged/BYOND/cfg"; - const string ByondDDConfig = "/daemon.txt"; - const string ByondNoPromptTrustedMode = "trusted-check 0"; - - TGByondStatus updateStat = TGByondStatus.Idle; - object ByondLock = new object(); - string lastError; - - Thread RevisionStaging; - - //Just cleanup - void InitByond() - { - CleanByondStaging(); - } - - void CleanByondStaging() - { - //linger not - if (File.Exists(RevisionDownloadPath)) - File.Delete(RevisionDownloadPath); - Program.DeleteDirectory(StagingDirectory); - } - - //Kill the thread and cleanup again - void DisposeByond() - { - lock (ByondLock) - { - if (RevisionStaging != null) - RevisionStaging.Abort(); - CleanByondStaging(); - } - } - - //requires ByondLock to be locked - bool BusyCheckNoLock() - { - switch (updateStat) - { - default: - case TGByondStatus.Starting: - case TGByondStatus.Downloading: - case TGByondStatus.Staging: - case TGByondStatus.Updating: - return true; - case TGByondStatus.Idle: - case TGByondStatus.Staged: - return false; - } - } - - //public api - public TGByondStatus CurrentStatus() - { - lock (ByondLock) - { - return updateStat; - } - } - - //public api - public string GetError() - { - lock (ByondLock) - { - var error = lastError; - lastError = null; - return error; - } - } - - //public api - public string GetVersion(TGByondVersion type) - { - try - { - lock (ByondLock) - { - if (type == TGByondVersion.Latest) - { - //get the latest version from the website - HttpWebRequest request = (HttpWebRequest)WebRequest.Create(ByondLatestURL); - var results = new List(); - using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) - { - using (StreamReader reader = new StreamReader(response.GetResponseStream())) - { - string html = reader.ReadToEnd(); - - Regex regex = new Regex("\\\"([^\"]*)\\\""); - MatchCollection matches = regex.Matches(html); - foreach (Match match in matches) - if (match.Success && match.Value.Contains("_byond.exe")) - results.Add(match.Value.Replace("\"", "").Replace("_byond.exe", "")); - } - } - results.Sort(); - results.Reverse(); - return results.Count > 0 ? results[0] : null; - } - else - { - string DirToUse = type == TGByondVersion.Staged ? StagingDirectoryInner : ByondDirectory; - if (Directory.Exists(DirToUse)) - { - string file = DirToUse + VersionFile; - if (File.Exists(file)) - return File.ReadAllText(file); - } - } - return null; - } - } - catch (Exception e) - { - return "Error: " + e.ToString(); - } - } - - //literally just for passing 2 ints to the thread function - class VersionInfo - { - public int major, minor; - } - - //does the downloading and unzipping - //calls ApplyStagedUpdate() after if the server isn't running - public void UpdateToVersionImpl(object param) - { - lock (ByondLock) { - if (updateStat != TGByondStatus.Starting) - return; - updateStat = TGByondStatus.Downloading; - } - - try - { - CleanByondStaging(); - - var vi = (VersionInfo)param; - using (var client = new WebClient()) - { - SendMessage(String.Format("BYOND: Updating to version {0}.{1}...", vi.major, vi.minor), ChatMessageType.DeveloperInfo); - - //DOWNLOADING - - try - { - client.DownloadFile(String.Format(ByondRevisionsURL, vi.major, vi.minor), RevisionDownloadPath); - } - catch - { - 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); - lock (ByondLock) - { - updateStat = TGByondStatus.Idle; - } - return; - } - } - lock (ByondLock) - { - updateStat = TGByondStatus.Staging; - } - - //STAGING - - ZipFile.ExtractToDirectory(RevisionDownloadPath, StagingDirectory); - lock (ByondLock) - { - File.WriteAllText(StagingDirectoryInner + VersionFile, String.Format("{0}.{1}", vi.major, vi.minor)); - //IMPORTANT: SET THE BYOND CONFIG TO NOT PROMPT FOR TRUSTED MODE REEE - Directory.CreateDirectory(ByondConfigDir); - File.WriteAllText(ByondConfigDir + ByondDDConfig, ByondNoPromptTrustedMode); - } - File.Delete(RevisionDownloadPath); - - lock (ByondLock) - { - updateStat = TGByondStatus.Staged; - } - - switch (DaemonStatus()) - { - case TGDreamDaemonStatus.Offline: - if(ApplyStagedUpdate()) - lastError = null; - else - lastError = "Failed to apply update!"; - break; - default: - 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); - break; - } - } - catch (ThreadAbortException) - { - return; - } - catch (Exception e) - { - TGServerService.WriteError("Revision staging errror: " + e.ToString(), TGServerService.EventID.BYONDUpdateFail); - lock (ByondLock) - { - updateStat = TGByondStatus.Idle; - lastError = e.ToString(); - RevisionStaging = null; - } - } - } - //public api for kicking off the update thread - public bool UpdateToVersion(int ma, int mi) - { - lock (ByondLock) - { - if (!BusyCheckNoLock()) - { - updateStat = TGByondStatus.Starting; - RevisionStaging = new Thread(new ParameterizedThreadStart(UpdateToVersionImpl)) - { - IsBackground = true //don't slow me down - }; - RevisionStaging.Start(new VersionInfo { major = ma, minor = mi }); - return true; - } - return false; - } - } - //tries to apply the staged update - public bool ApplyStagedUpdate() - { - lock (CompilerLock) - { - if (compilerCurrentStatus == TGCompilerStatus.Compiling) - return false; - lock (ByondLock) - { - if (updateStat != TGByondStatus.Staged) - return false; - updateStat = TGByondStatus.Updating; - } - try - { - Program.DeleteDirectory(ByondDirectory); - Directory.Move(StagingDirectoryInner, ByondDirectory); - 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); - return true; - } - catch (Exception e) - { - lastError = e.ToString(); - SendMessage("BYOND: Update failed!", ChatMessageType.DeveloperInfo); - TGServerService.WriteError("BYOND update failed!", TGServerService.EventID.BYONDUpdateFail); - return false; - } - finally - { - lock(ByondLock) { - updateStat = TGByondStatus.Idle; - } - } - } - } - } -} diff --git a/TGServerService/Chat.cs b/TGServerService/Chat.cs deleted file mode 100644 index 12ced80fa9..0000000000 --- a/TGServerService/Chat.cs +++ /dev/null @@ -1,292 +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 - /// true if the bot was mentioned in the first word, false 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..51a4014329 100644 --- a/TGServerService/ChatCommands.cs +++ b/TGServerService/ChatCommands.cs @@ -3,20 +3,49 @@ using System; using System.Collections.Generic; using System.Threading; -namespace TGServerService +namespace TGServerService.ChatCommands { - class CommandInfo + /// + /// Metadata about the currently running + /// + sealed class CommandInfo { + /// + /// If the was invoked by an admin + /// public bool IsAdmin { get; set; } + /// + /// If the was invoked from an admin chat channel + /// public bool IsAdminChannel { get; set; } + /// + /// The name of the invoker + /// public string Speaker { get; set; } - public TGStationServer Server { get; set; } + /// + /// A reference to the that runs the that heard the + /// + public ServerInstance Server { get; set; } } + /// + /// A command heard by a + /// abstract class ChatCommand : Command { + /// + /// for the + /// + public static ThreadLocal CommandInfo { get; private set; } = new ThreadLocal(); + /// + /// If set to , the cannot be invoked by a non-admin or outside an admin chat channel + /// public bool RequiresAdmin { get; protected set; } - public static ThreadLocal CommandInfo = new ThreadLocal(); - protected TGStationServer Instance { get { return CommandInfo.Value.Server; } } + /// + /// Shorthand for accessing + /// + protected ServerInstance Instance { get { return CommandInfo.Value.Server; } } + + /// public override ExitCode DoRun(IList parameters) { if (RequiresAdmin) @@ -37,9 +66,23 @@ namespace TGServerService } } - class ServerChatCommand : ChatCommand + /// + /// s generated by DreamDaemon via the API + /// + sealed class ServerChatCommand : ChatCommand { + /// + /// The help text for the + /// readonly string HelpText; + + /// + /// Construct a + /// + /// The invocation of the + /// The help text of the + /// If set to , the cannot be invoked by a non-admin or outside an admin chat channel + /// The number of parameters the requires public ServerChatCommand(string name, string helpText, bool adminOnly, int requiredParameters) { Keyword = name; @@ -48,22 +91,31 @@ namespace TGServerService RequiredParameters = requiredParameters; } + /// public override string GetHelpText() { return HelpText; } + /// 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; } } - class RootChatCommand : RootCommand + /// + /// The main root chat command + /// + sealed class RootChatCommand : RootCommand { + /// + /// Construct a + /// + /// List of s supplied by DreamDaemon public RootChatCommand(List serverCommands) { var tmp = new List { new PRsCommand(), new VersionCommand(), new RevisionCommand(), new ByondCommand(), new KekCommand() }; @@ -74,12 +126,19 @@ namespace TGServerService PrintHelpList = true; } } - class RevisionCommand : ChatCommand + /// + /// Retrieves the git SHA of the live DreamDaemon code + /// + sealed class RevisionCommand : ChatCommand { + /// + /// Construct a + /// public RevisionCommand() { Keyword = "revision"; } + /// protected override ExitCode Run(IList parameters) { var res = Instance.LiveSha(); @@ -91,79 +150,116 @@ namespace TGServerService return ExitCode.Normal; } + /// public override string GetHelpText() { return "Prints the current code revision of the repository (not the server)"; } } - - class ByondCommand : ChatCommand + /// + /// Retrieve the installed, staged, or latest availab + /// + sealed class ByondCommand : ChatCommand { + /// + /// Construct a + /// public ByondCommand() { Keyword = "byond"; } + + /// 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; } + /// public override string GetHelpText() { return "Gets the specified BYOND version"; } + + /// public override string GetArgumentString() { return "[--staged|--latest]"; } } - class VersionCommand : ChatCommand + /// + /// Retrieve the current service version + /// + sealed class VersionCommand : ChatCommand { + /// + /// Construct a + /// public VersionCommand() { Keyword = "version"; } + + /// protected override ExitCode Run(IList parameters) { OutputProc(Instance.Version()); return ExitCode.Normal; } + /// public override string GetHelpText() { return "Gets the running service version"; } } - class KekCommand : ChatCommand + /// + /// kek + /// + sealed class KekCommand : ChatCommand { + /// + /// Construct a + /// public KekCommand() { Keyword = "kek"; } + + /// protected override ExitCode Run(IList parameters) { OutputProc("kek"); return ExitCode.Normal; } + /// public override string GetHelpText() { return "kek"; } } - class PRsCommand : ChatCommand + /// + /// Retrieve the list of test-merged github pull requests + /// + sealed class PRsCommand : ChatCommand { + /// + /// Construct a + /// public PRsCommand() { Keyword = "prs"; } + + /// protected override ExitCode Run(IList parameters) { var PRs = Instance.MergedPullRequests(out string res); @@ -184,6 +280,7 @@ namespace TGServerService return ExitCode.Normal; } + /// public override string GetHelpText() { return "Gets the currently merged pull requests in the repository"; diff --git a/TGServerService/ChatProviders/ChatProvider.cs b/TGServerService/ChatProviders/ChatProvider.cs new file mode 100644 index 0000000000..da6ff62999 --- /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 + /// + /// 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(IChatProvider ChatProvider, string speaker, string channel, string message, bool isAdmin, bool isAdminChannel); + /// + /// Interface for a chat provder service + /// + interface IChatProvider : IDisposable + { + /// + /// Sets for the provider + /// + /// The to set + /// null on success, error message on failure + string SetProviderInfo(ChatSetupInfo info); + + /// + /// Gets the info of the provider + /// + /// The for the chat provider + ChatSetupInfo ProviderInfo(); + + /// + /// Called with chat message info + /// + event OnChatMessage OnChatMessage; + + /// + /// Connects the chat provider if it's enabled + /// + /// on success, error message on failure + string Connect(); + /// + /// Forces a reconnection of the chat provider if it's enabled + /// + /// on success, error message on failure + string Reconnect(); + + /// + /// Checks if the chat provider is connected + /// + /// if the provider is connected, otherwise + bool Connected(); + + /// + /// Disconnects the chat provider + /// + void Disconnect(); + + /// + /// Send a to a + /// + /// The message to send + /// The channel to send to + /// on success, error message on failure + string SendMessageDirect(string message, string channel); + + /// + /// Broadcast a to appropriate channels based on the message type + /// + /// The message to send + /// The + void SendMessage(string message, MessageType mt); + } +} diff --git a/TGServerService/Discord.cs b/TGServerService/ChatProviders/DiscordChatProvider.cs similarity index 60% rename from TGServerService/Discord.cs rename to TGServerService/ChatProviders/DiscordChatProvider.cs index 87758d74e5..1fb5fa3c77 100644 --- a/TGServerService/Discord.cs +++ b/TGServerService/ChatProviders/DiscordChatProvider.cs @@ -6,45 +6,80 @@ using System.Threading; using System.Threading.Tasks; using TGServiceInterface; -namespace TGServerService +namespace TGServerService.ChatProviders { - class TGDiscordChatProvider : ITGChatProvider + /// + /// for Discord: https://discordapp.com/ + /// + sealed class DiscordChatProvider : IChatProvider { + /// public event OnChatMessage OnChatMessage; + /// + /// The Discord API client + /// DiscordSocketClient client; - TGDiscordSetupInfo DiscordConfig; + /// + /// The setup info for the provider + /// + DiscordSetupInfo DiscordConfig; + /// + /// Used for multithreading safety + /// object DiscordLock = new object(); + /// + /// An of internal identifers => s we have seen + /// IDictionary SeenPrivateChannels = new Dictionary(); - public TGDiscordChatProvider(TGChatSetupInfo info) + /// + /// Construct a + /// + /// The + public DiscordChatProvider(ChatSetupInfo info) { Init(info); } - - public TGChatSetupInfo ProviderInfo() + + /// + public ChatSetupInfo ProviderInfo() { return DiscordConfig; } - void Init(TGChatSetupInfo info) + /// + /// Sets up the Discord API and + /// + /// The to init with + void Init(ChatSetupInfo info) { - DiscordConfig = new TGDiscordSetupInfo(info); + DiscordConfig = new DiscordSetupInfo(info); client = new DiscordSocketClient(); client.MessageReceived += Client_MessageReceived; } - private bool CheckAdmin(SocketUser u) + /// + /// Checks if a is considered a chat admin + /// + /// The sender of a message + /// if is a chat admin, otherwise + private bool CheckAdmin(SocketUser user) { if (!DiscordConfig.AdminsAreSpecial) - return DiscordConfig.AdminList.Contains(u.Id.ToString()); - if(u is SocketGuildUser sgu) + return DiscordConfig.AdminList.Contains(user.Id.ToString()); + if(user is SocketGuildUser sgu) foreach (var I in sgu.Roles) if (DiscordConfig.AdminList.Contains(I.Id.ToString())) return true; return false; } + /// + /// Called when a channel the bot is in recieves a message or the bot is PM'd directly + /// + /// The event arguments + /// The task to run when this occurs private async Task Client_MessageReceived(SocketMessage e) { await Task.Run(() => @@ -81,6 +116,7 @@ namespace TGServerService }); } + /// public string Connect() { try @@ -102,6 +138,7 @@ namespace TGServerService } } + /// public bool Connected() { lock (DiscordLock) @@ -110,6 +147,7 @@ namespace TGServerService } } + /// public void Disconnect() { try @@ -126,13 +164,15 @@ namespace TGServerService catch { } } + /// public string Reconnect() { Disconnect(); return Connect(); } - public void SendMessage(string msg, ChatMessageType mt) + /// + public void SendMessage(string msg, MessageType mt) { if (!Connected()) return; @@ -145,10 +185,10 @@ namespace TGServerService { var cid = J.Id.ToString(); var wdc = DiscordConfig.WatchdogChannels; - bool SendToThisChannel = (mt.HasFlag(ChatMessageType.AdminInfo) && DiscordConfig.AdminChannels.Contains(cid)) - || (mt.HasFlag(ChatMessageType.DeveloperInfo) && DiscordConfig.DevChannels.Contains(cid)) - || (mt.HasFlag(ChatMessageType.GameInfo) && DiscordConfig.GameChannels.Contains(cid)) - || (mt.HasFlag(ChatMessageType.WatchdogInfo) && DiscordConfig.WatchdogChannels.Contains(cid)); + bool SendToThisChannel = (mt.HasFlag(MessageType.AdminInfo) && DiscordConfig.AdminChannels.Contains(cid)) + || (mt.HasFlag(MessageType.DeveloperInfo) && DiscordConfig.DevChannels.Contains(cid)) + || (mt.HasFlag(MessageType.GameInfo) && DiscordConfig.GameChannels.Contains(cid)) + || (mt.HasFlag(MessageType.WatchdogInfo) && DiscordConfig.WatchdogChannels.Contains(cid)); if (SendToThisChannel) tasks.Add(J.SendMessageAsync(msg)); @@ -158,6 +198,7 @@ namespace TGServerService } } + /// public string SendMessageDirect(string message, string channelname) { if (!Connected()) @@ -176,7 +217,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; } } @@ -185,6 +226,10 @@ namespace TGServerService return e.ToString(); } } + + /// + /// Shutsdown and disposes + /// void DisconnectAndDispose() { try @@ -193,19 +238,20 @@ 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(); @@ -228,9 +274,16 @@ namespace TGServerService } #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls + /// + /// To detect redundant calls + /// + private bool disposedValue = false; - protected virtual void Dispose(bool disposing) + /// + /// Implements the pattern. Calls + /// + /// if was called manually, if it was from the finalizer + void Dispose(bool disposing) { if (!disposedValue) { @@ -252,8 +305,9 @@ namespace TGServerService // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. // Dispose(false); // } - - // This code added to correctly implement the disposable pattern. + /// + /// Implements the pattern + /// public void Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. diff --git a/TGServerService/IRC.cs b/TGServerService/ChatProviders/IRCChatProvider.cs similarity index 62% rename from TGServerService/IRC.cs rename to TGServerService/ChatProviders/IRCChatProvider.cs index 7194be267c..b5e73ac1c1 100644 --- a/TGServerService/IRC.cs +++ b/TGServerService/ChatProviders/IRCChatProvider.cs @@ -4,32 +4,51 @@ using System.Threading; using TGServiceInterface; using Meebey.SmartIrc4net; - -namespace TGServerService +namespace TGServerService.ChatProviders { - class TGIRCChatProvider : ITGChatProvider + /// + /// for internet relay chat + /// + sealed class IRCChatProvider : IChatProvider { + /// + /// Header used to mark that a channel is actually a query message + /// const string PrivateMessageMarker = "---PRIVATE-MSG---"; + /// + /// The irc client + /// IrcFeatures irc; - + /// + /// Used for multithreading safety + /// object IRCLock = new object(); - TGIRCSetupInfo IRCConfig; + /// + /// The setup info for the provider + /// + IRCSetupInfo IRCConfig; + /// public event OnChatMessage OnChatMessage; - - public TGChatSetupInfo ProviderInfo() + + /// + public ChatSetupInfo ProviderInfo() { return IRCConfig; } - public TGIRCChatProvider(TGChatSetupInfo info) + /// + /// Construct a + /// + /// The + public IRCChatProvider(ChatSetupInfo info) { - IRCConfig = new TGIRCSetupInfo(info); + IRCConfig = new IRCSetupInfo(info); irc = new IrcFeatures() { SupportNonRfc = true, - CtcpUserInfo = TGServerService.Version, + CtcpUserInfo = Service.VersionString, AutoRejoin = true, AutoRejoinOnKick = true, AutoRelogin = true, @@ -41,8 +60,8 @@ namespace TGServerService irc.OnChannelMessage += Irc_OnChannelMessage; irc.OnQueryMessage += Irc_OnQueryMessage; } - - //public api + + /// public string SendMessageDirect(string message, string channel) { try @@ -55,7 +74,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 +83,10 @@ 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) @@ -84,10 +104,17 @@ namespace TGServerService return null; } + /// + /// Checks if a message is considered sent from a chat admin + /// + /// The + /// if was sent by a chat admin, otherwise private bool CheckAdmin(IrcMessageData e) { if (IRCConfig.AdminsAreSpecial) { + if (e.Channel == null) + return false; var user = (NonRfcChannelUser)irc.GetChannelUser(e.Channel, e.Nick); if (user != null) switch (IRCConfig.AuthLevel) @@ -120,12 +147,21 @@ namespace TGServerService return false; } - //private message + /// + /// Called when the bot recieves a query message + /// + /// The sender of the event (usually ) + /// The private void Irc_OnQueryMessage(object sender, IrcEventArgs e) { OnChatMessage(this, e.Data.Nick, e.Data.Nick + PrivateMessageMarker, e.Data.Message, CheckAdmin(e.Data), true); } + /// + /// Called when a channel the bot is in recieves a message + /// + /// The sender of the event (usually ) + /// The private void Irc_OnChannelMessage(object sender, IrcEventArgs e) { var formattedMessage = e.Data.Message.Trim(); @@ -142,7 +178,10 @@ namespace TGServerService OnChatMessage(this, e.Data.Nick, e.Data.Channel, formattedMessage, CheckAdmin(e.Data), IRCConfig.AdminChannels.Contains(e.Data.Channel.ToLower())); } - //Joins configured channels + + /// + /// Joins all channels specified in + /// void JoinChannels() { var hs = new HashSet(); //for unique inserts @@ -163,7 +202,10 @@ namespace TGServerService foreach (var I in hs) irc.RfcJoin(I); } - //runs the login command + + /// + /// Sends a login query to with message + /// void Login() { lock (IRCLock) @@ -172,7 +214,7 @@ namespace TGServerService irc.SendMessage(SendType.Message, IRCConfig.AuthTarget, IRCConfig.AuthMessage); } } - //public api + /// public string Connect() { if (Connected() || !IRCConfig.Enabled) @@ -211,7 +253,9 @@ namespace TGServerService } } - //This is the thread that listens for irc messages + /// + /// Runs the listener in a safe loop + /// void IRCListen() { while (irc != null && Connected()) @@ -222,14 +266,14 @@ namespace TGServerService catch { } } - //public api + /// public string Reconnect() { Disconnect(); return Connect(); } - //public api + /// public void Disconnect() { try @@ -245,10 +289,10 @@ 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 + /// public bool Connected() { lock (IRCLock) @@ -256,30 +300,40 @@ namespace TGServerService return irc != null && irc.IsConnected; } } - //public api - public void SendMessage(string message, ChatMessageType mt) + /// + public void SendMessage(string message, MessageType mt) { if (!Connected()) return; lock (IRCLock) { - foreach (var cid in irc.JoinedChannels) + string cids = ""; + for (var I = 0; I < irc.JoinedChannels.Count; ++I) { - bool SendToThisChannel = (mt.HasFlag(ChatMessageType.AdminInfo) && IRCConfig.AdminChannels.Contains(cid)) - || (mt.HasFlag(ChatMessageType.DeveloperInfo) && IRCConfig.DevChannels.Contains(cid)) - || (mt.HasFlag(ChatMessageType.GameInfo) && IRCConfig.GameChannels.Contains(cid)) - || (mt.HasFlag(ChatMessageType.WatchdogInfo) && IRCConfig.WatchdogChannels.Contains(cid)); + var cid = irc.JoinedChannels[I]; + bool SendToThisChannel = (mt.HasFlag(MessageType.AdminInfo) && IRCConfig.AdminChannels.Contains(cid)) + || (mt.HasFlag(MessageType.DeveloperInfo) && IRCConfig.DevChannels.Contains(cid)) + || (mt.HasFlag(MessageType.GameInfo) && IRCConfig.GameChannels.Contains(cid)) + || (mt.HasFlag(MessageType.WatchdogInfo) && IRCConfig.WatchdogChannels.Contains(cid)); if (SendToThisChannel) - irc.SendMessage(SendType.Message, cid, message); + cids += I > 0 ? ',' + cid : cid; } + irc.SendMessage(SendType.Message, cids, message); } } #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls + /// + /// To detect redundant calls + /// + private bool disposedValue = false; - protected virtual void Dispose(bool disposing) + /// + /// Implements the pattern. Calls and sets to + /// + /// if was called manually, if it was from the finalizer + void Dispose(bool disposing) { if (!disposedValue) { @@ -304,6 +358,9 @@ namespace TGServerService // } // This code added to correctly implement the disposable pattern. + /// + /// Implements the pattern + /// public void Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. diff --git a/TGServerService/DeprecatedInstanceConfig.cs b/TGServerService/DeprecatedInstanceConfig.cs index 221e7f2fb6..8376f4b983 100644 --- a/TGServerService/DeprecatedInstanceConfig.cs +++ b/TGServerService/DeprecatedInstanceConfig.cs @@ -1,13 +1,58 @@ -namespace TGServerService +using TGServiceInterface; + +namespace TGServerService { - //since we can't quite remove old config options this is here as a dumping ground for them + /// + /// Used to migrate old config settings + /// class DeprecatedInstanceConfig : InstanceConfig { - //do not use CurrentVersion in this function - //simply migrate from Version to Version + 1 - public void Migrate() + /// + /// Convert the 3.1 .NET settings file to a config json + /// + /// A saved based off the old .NET setting file + public static InstanceConfig CreateFromNETSettings() { + var Config = Properties.Settings.Default; + var result = new DeprecatedInstanceConfig() + { + InstanceDir = (string)Config.GetPreviousVersion("ServerDirectory"), + ProjectName = (string)Config.GetPreviousVersion("ProjectName"), + Port = (ushort)Config.GetPreviousVersion("ServerPort"), + CommitterName = (string)Config.GetPreviousVersion("CommitterName"), + CommitterEmail = (string)Config.GetPreviousVersion("CommitterEmail"), + Security = (DreamDaemonSecurity)Config.GetPreviousVersion("ServerSecurity"), + Autostart = (bool)Config.GetPreviousVersion("DDAutoStart"), + ChatProviderData = (string)Config.GetPreviousVersion("ChatProviderData"), + ChatProviderEntropy = (string)Config.GetPreviousVersion("ChatProviderEntropy"), + ReattachRequired = (bool)Config.GetPreviousVersion("ReattachToDD"), + ReattachProcessID = (int)Config.GetPreviousVersion("ReattachPID"), + ReattachPort = (ushort)Config.GetPreviousVersion("ReattachPort"), + ReattachCommsKey = (string)Config.GetPreviousVersion("ReattachCommsKey"), + ReattachAPIVersion = (string)Config.GetPreviousVersion("ReattachAPIVersion"), + AutoUpdateInterval = (ulong)Config.GetPreviousVersion("AutoUpdateInterval"), + AuthorizedUserGroupSID = (string)Config.GetPreviousVersion("AuthorizedGroupSID") + }; + result.MigrateToCurrentVersion(); + result.Save(); + return result; + } + /// + /// Migrates the from to + /// + public void MigrateToCurrentVersion() + { + for (; Version < CurrentVersion; ++Version) + Migrate(); + } + + /// + /// Migrates the from to + 1 + /// + void Migrate() + { + //Not needed so far } } } diff --git a/TGServerService/DreamDaemon.cs b/TGServerService/DreamDaemon.cs deleted file mode 100644 index 27775f3e61..0000000000 --- a/TGServerService/DreamDaemon.cs +++ /dev/null @@ -1,539 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Reflection; -using System.Threading; -using TGServiceInterface; - -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 - { - enum ShutdownRequestPhase - { - None, - Requested, - Pinged, - } - - const int DDHangStartTime = 60; - const int DDBadStartTime = 10; - - Process Proc; - - object watchdogLock = new object(); - Thread DDWatchdog; - TGDreamDaemonStatus currentStatus; - ushort currentPort = 0; - - object restartLock = new object(); - bool RestartInProgress = false; - - TGDreamDaemonSecurity StartingSecurity; - - ShutdownRequestPhase AwaitingShutdown; - - //Only need 1 proc instance - void InitDreamDaemon() - { - var Reattach = Config.ReattachRequired; - if (Reattach) - try - { - Proc = Process.GetProcessById(Config.ReattachProcessID); - if (Proc == null) - throw new Exception("GetProcessById returned null!"); - TGServerService.WriteInfo("Reattached to running DD process!", TGServerService.EventID.DDReattachSuccess); - ThreadPool.QueueUserWorkItem(_ => - { - Thread.Sleep(5000); - SendMessage("DD: Update complete. Watch dog reactivated...", ChatMessageType.WatchdogInfo); - }); - - //start wd - RestartInProgress = true; - currentPort = Config.ReattachPort; - serviceCommsKey = Config.ReattachCommsKey; - try - { - GameAPIVersion = new Version(Config.ReattachAPIVersion); - } - catch { } - currentStatus = TGDreamDaemonStatus.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}", Config.ReattachRequired, e.ToString()), TGServerService.EventID.DDReattachFail); - } - finally - { - Config.ReattachRequired = false; - Config.Save(); - } - - if (Proc == null) - Proc = new Process(); - - Proc.StartInfo.FileName = ByondDirectory + "/bin/dreamdaemon.exe"; - Proc.StartInfo.UseShellExecute = false; - - if (Reattach) - return; - - //autostart the server - if (Config.Autostart) - //break this off so we don't hold up starting the service - ThreadPool.QueueUserWorkItem( _ => { Start(); }); - } - - //die now k thx - void DisposeDreamDaemon() - { - var Detach = Config.ReattachRequired; - if (DaemonStatus() == TGDreamDaemonStatus.Online) - { - if (!Detach) - { - WorldAnnounce("Server service stopped"); - Thread.Sleep(1000); - } - else - SendMessage("DD: Detaching watch dog for update!", ChatMessageType.WatchdogInfo); - } - else if (Detach) - { - Config.ReattachRequired = false; - } - Stop(); - } - - //public api - public TGDreamDaemonStatus DaemonStatus() - { - lock (watchdogLock) - { - return currentStatus; - } - } - - //public api - public void RequestRestart() - { - SendCommand(SCHardReboot); - } - - //public api - public void RequestStop() - { - lock (watchdogLock) - { - if (currentStatus != TGDreamDaemonStatus.Online || AwaitingShutdown != ShutdownRequestPhase.None) - return; - AwaitingShutdown = ShutdownRequestPhase.Pinged; - } - SendCommand(SCGracefulShutdown); - } - - //public api - public string Stop() - { - Thread t; - lock (watchdogLock) - { - t = DDWatchdog; - DDWatchdog = null; - } - if (t != null && t.IsAlive) - { - t.Abort(); - t.Join(); - return null; - } - else - return "Server not running"; - } - - //public api - public void SetPort(ushort new_port) - { - lock (watchdogLock) - { - Config.Port = new_port; - RequestRestart(); - } - } - - //handle a kill request from the server - public void KillMe() - { - bool DoRestart; - lock (watchdogLock) - { - DoRestart = AwaitingShutdown == ShutdownRequestPhase.None; - if (!DoRestart) - AwaitingShutdown = ShutdownRequestPhase.Pinged; - } - //Do this is a seperate thread or we'll kill this thread in the middle of rebooting - if (DoRestart) - ThreadPool.QueueUserWorkItem(_ => { Restart(); }); - else - ThreadPool.QueueUserWorkItem(_ => { Stop(); }); - } - - //public api - public string Restart() - { - if (DaemonStatus() == TGDreamDaemonStatus.Offline) - return Start(); - lock(restartLock) - { - if (RestartInProgress) - return "Restart already in progress"; - RestartInProgress = true; - } - SendMessage("DD: Hard restart triggered", ChatMessageType.WatchdogInfo); - Stop(); - var res = Start(); - if(res != null) - lock(restartLock) - { - RestartInProgress = false; - } - return res; - } - - //loop that keeps the server running - void Watchdog() - { - try - { - lock (restartLock) - { - if (!RestartInProgress) - { - SendMessage("DD: Server started, watchdog active...", ChatMessageType.WatchdogInfo); - TGServerService.WriteInfo("Watchdog started", TGServerService.EventID.DDWatchdogStarted); - } - else - { - RestartInProgress = false; - TGServerService.WriteInfo("Watchdog started", TGServerService.EventID.DDWatchdogRestarted); - } - } - var retries = 0; - while (true) - { - var starttime = DateTime.Now; - - lock (watchdogLock) - { - if (AwaitingShutdown == ShutdownRequestPhase.Requested) - SendCommand(SCGracefulShutdown); - } - - Proc.WaitForExit(); - - lock (watchdogLock) - { - currentStatus = TGDreamDaemonStatus.HardRebooting; - currentPort = 0; - Proc.Close(); - - if (AwaitingShutdown == ShutdownRequestPhase.Pinged) - return; - - if ((DateTime.Now - starttime).TotalSeconds < DDBadStartTime) - { - ++retries; - var sleep_time = (int)Math.Min(Math.Pow(2, retries), 3600); //max of one hour - SendMessage(String.Format("DD: Watchdog server startup failed! Retrying in {0} seconds...", sleep_time), ChatMessageType.WatchdogInfo); - Thread.Sleep(sleep_time * 1000); - } - else - { - retries = 0; - var msg = "DD: DreamDaemon crashed! Watchdog rebooting DD..."; - SendMessage(msg, ChatMessageType.WatchdogInfo); - TGServerService.WriteWarning(msg, TGServerService.EventID.DDWatchdogRebootingServer); - } - } - - var res = StartImpl(true); - if (res != null) - throw new Exception("Hard restart failed: " + res); - } - } - catch (ThreadAbortException) - { - //No Mr bond, I expect you to die - try - { - if (!Config.ReattachRequired) - { - Proc.Kill(); - Proc.WaitForExit(); - } - else - { - Config.ReattachProcessID = Proc.Id; - Config.ReattachPort = currentPort; - Config.ReattachCommsKey = serviceCommsKey; - RestartInProgress = true; - } - Proc.Close(); - } - catch - { } - } - catch (Exception e) - { - SendMessage("DD: Watchdog thread crashed!", ChatMessageType.WatchdogInfo); - TGServerService.WriteError("Watch dog thread crashed: " + e.ToString(), TGServerService.EventID.DDWatchdogCrash); - } - finally - { - lock (watchdogLock) - { - currentStatus = TGDreamDaemonStatus.Offline; - currentPort = 0; - AwaitingShutdown = ShutdownRequestPhase.None; - if (!RestartInProgress) - { - if(!Config.ReattachRequired) - SendMessage("DD: Server stopped, watchdog exiting...", ChatMessageType.WatchdogInfo); - TGServerService.WriteInfo("Watch dog exited", TGServerService.EventID.DDWatchdogExit); - } - else - TGServerService.WriteInfo("Watch dog restarting...", TGServerService.EventID.DDWatchdogRestart); - } - } - } - - //public api - public string CanStart() - { - lock (watchdogLock) - { - return CanStartImpl(); - } - } - - string CanStartImpl() - { - if (GetVersion(TGByondVersion.Installed) == null) - return "Byond is not installed!"; - var DMB = GameDirLive + "/" + Config.ProjectName + ".dmb"; - if (!File.Exists(DMB)) - return String.Format("Unable to find {0}!", DMB); - return null; - } - - //public api - public string Start() - { - if (CurrentStatus() == TGByondStatus.Staged) - { - //IMPORTANT: SLEEP FOR A MOMENT OR WONDOWS WON'T RELEASE THE FUCKING BYOND DLL HANDLES!!!! REEEEEEE - Thread.Sleep(3000); - ApplyStagedUpdate(); - } - lock (watchdogLock) - { - if (currentStatus != TGDreamDaemonStatus.Offline) - return "Server already running"; - var res = CanStartImpl(); - if (res != null) - return res; - currentPort = 0; - currentStatus = TGDreamDaemonStatus.HardRebooting; - } - return StartImpl(false); - } - - //translate the configured security level into a byond param - string SecurityWord(bool starting = false) - { - var level = starting ? StartingSecurity : (TGDreamDaemonSecurity)Config.Security; - switch (level) - { - case TGDreamDaemonSecurity.Safe: - return "safe"; - case TGDreamDaemonSecurity.Trusted: - return "trusted"; - case TGDreamDaemonSecurity.Ultrasafe: - return "ultrasafe"; - default: - throw new Exception(String.Format("Bad DreamDaemon security level: {0}", level)); - } - } - - void UpdateInterfaceDll(bool overwrite) - { - if (File.Exists(InterfaceDLLName) && !overwrite) - return; - //Copy the interface dll to the static dir - var InterfacePath = Assembly.GetAssembly(typeof(DDInteropCallHolder)).Location; - File.Copy(InterfacePath, InterfaceDLLName, overwrite); - } - - //used by Start and Watchdog to start a DD instance - string StartImpl(bool watchdog) - { - try - { - lock (watchdogLock) - { - var res = CanStartImpl(); - if (res != null) - return res; - - var DMB = GameDirLive + "/" + Config.ProjectName + ".dmb"; - - GenCommsKey(); - StartingSecurity = (TGDreamDaemonSecurity)Config.Security; - Proc.StartInfo.Arguments = String.Format("{0} -port {1} {5}-close -verbose -params \"server_service={3}&server_service_version={4}\" -{2} -public", DMB, Config.Port, SecurityWord(), serviceCommsKey, Version(), Config.Webclient ? "-webclient " : ""); - UpdateInterfaceDll(true); - lock (topicLock) - { - GameAPIVersion = null; //needs updating - } - Proc.Start(); - - if (!Proc.WaitForInputIdle(DDHangStartTime * 1000)) - { - Proc.Kill(); - Proc.WaitForExit(); - Proc.Close(); - currentStatus = TGDreamDaemonStatus.Offline; - currentPort = 0; - return String.Format("Server start is taking more than {0}s! Aborting!", DDHangStartTime); - } - currentPort = Config.Port; - currentStatus = TGDreamDaemonStatus.Online; - if (!watchdog) - { - DDWatchdog = new Thread(new ThreadStart(Watchdog)); - DDWatchdog.Start(); - } - return null; - } - } - catch (Exception e) - { - currentStatus = TGDreamDaemonStatus.Offline; - return e.ToString(); - } - } - - //public api - public TGDreamDaemonSecurity SecurityLevel() - { - return (TGDreamDaemonSecurity)Config.Security; - } - - - //public api - public bool SetSecurityLevel(TGDreamDaemonSecurity level) - { - if (Config.Security == level) - return false; - Config.Security = level; - RequestRestart(); - return DaemonStatus() != TGDreamDaemonStatus.Online; - } - - //public api - public bool Autostart() - { - return Config.Autostart; - } - - //public api - public void SetAutostart(bool on) - { - Config.Autostart = on; - } - - //public api - public string StatusString(bool includeMetaInfo) - { - const string visSecStr = " (Sec: {0})"; - string res; - var ds = DaemonStatus(); - switch (ds) - { - case TGDreamDaemonStatus.Offline: - res = "OFFLINE"; - break; - case TGDreamDaemonStatus.HardRebooting: - res = "REBOOTING"; - break; - case TGDreamDaemonStatus.Online: - res = "ONLINE"; - if (includeMetaInfo) - { - string secandvis; - lock (watchdogLock) - { - secandvis = String.Format(visSecStr, SecurityWord(true)); - } - res += secandvis; - } - break; - default: - res = "NULL AND ERRORS"; - break; - } - if (includeMetaInfo && ds != TGDreamDaemonStatus.Online) - res += String.Format(visSecStr, SecurityWord()); - return res; - } - - //public api - public ushort Port() - { - return Config.Port; - } - - //public api - public bool ShutdownInProgress() - { - lock (watchdogLock) - { - return AwaitingShutdown != ShutdownRequestPhase.None; - } - } - - /// - public string WorldAnnounce(string message) - { - var res = SendCommand(SCWorldAnnounce + ";message=" + SanitizeTopicString(message)); - if (res == "SUCCESS") - return null; - return res; - } - - /// - public bool Webclient() - { - return Config.Webclient; - } - - /// - public void SetWebclient(bool on) - { - lock (watchdogLock) { - var diff = on != Config.Webclient; - if (diff) - { - Config.Webclient = on; - RequestRestart(); - } - } - } - } -} diff --git a/TGServerService/EventID.cs b/TGServerService/EventID.cs new file mode 100644 index 0000000000..867148150d --- /dev/null +++ b/TGServerService/EventID.cs @@ -0,0 +1,319 @@ +using System; + +namespace TGServerService +{ + /// + /// Various events and their IDs in no particular order. Found in the Windows event log. These incremented by 100 and are guaranteed to never be reused in the future. Each event ID may be information, a warning, or error and will be documented accordingly. Warnings will occur due to user, data, or network errors. Errors will occur due to filesystem errors or hard faults + /// + public enum EventID : int + { + /// + /// Info: When the bot recieves a (not necessarily valid) chat command + /// + ChatCommand = 100, + /// + /// Warning: When a fails to + /// + ChatConnectFail = 200, + /// + /// Error: When a fails to construct + /// + ChatProviderStartFail = 300, + /// + /// Error: When a bad is passed + /// + InvalidChatProvider = 400, + /// + /// Not in use anymore + /// + [Obsolete("Not in use anymore", true)] + UpdateRequest = 500, + /// + /// Warning: When the BYOND updater cannot download a revision. Error: When the BYOND updater cannot unzip or apply a revision + /// + BYONDUpdateFail = 600, + /// + /// Info: When the BYOND updater successfully staged a revision but could not apply it due to the being active + /// + BYONDUpdateStaged = 700, + /// + /// Info: When the BYOND updater successfully applies an update + /// + BYONDUpdateComplete = 800, + /// + /// Error: Failed to move the with + /// + ServerMoveFailed = 900, + /// + /// Warning: Failed to delete the old directory during a operation + /// + ServerMovePartial = 1000, + /// + /// Info: Successful completion of a operation + /// + ServerMoveComplete = 1100, + /// + /// Error: An internal error occurred during a operation + /// + DMCompileCrash = 1200, + /// + /// Error: An internal error occurred during a operation + /// + DMInitializeCrash = 1300, + /// + /// Warning: Compile failure of the target .dme in a operation + /// + DMCompileError = 1400, + /// + /// Info: Successful completion of a operation + /// + DMCompileSuccess = 1500, + /// + /// Info: Successful completion of a operation + /// + DMCompileCancel = 1600, + /// + /// Error: Failed to reattach the watchdog to a running DreamDaemon instance after a update + /// + DDReattachFail = 1700, + /// + /// Info: Successfully reattached the watchdog to a running DreamDaemon instance after a update + /// + DDReattachSuccess = 1800, + /// + /// Error: An internal error occurred while running the DreamDaemon watchdog + /// + DDWatchdogCrash = 1900, + /// + /// Info: The watchdog has exited, either for an or , or operation + /// + DDWatchdogExit = 2000, + /// + /// Not in use anymore + /// + [Obsolete("Not in use anymore", true)] + DDWatchdogRebootedServer = 2100, + /// + /// Warning: DreamDaemon has crashed and the watchdog is attempting to reboot it + /// + DDWatchdogRebootingServer = 2200, + /// + /// Info: The watchdog is performing a operation + /// + DDWatchdogRestart = 2300, + /// + /// Info: Successful completion of a operation + /// + DDWatchdogRestarted = 2400, + /// + /// Info: Successful completion of a operation + /// + DDWatchdogStarted = 2500, + /// + /// Info: Successful completion of a operation + /// + ChatSend = 2600, + /// + /// Info: Successful completion of a operation + /// + ChatBroadcast = 2700, + /// + /// Not in use anymore + /// + [Obsolete("Not in use anymore", true)] + ChatAdminBroadcast = 2800, + /// + /// Error: When an error occurs during a operation + /// + ChatDisconnectFail = 2900, + /// + /// Not in use anymore + /// + [Obsolete("Not in use anymore", true)] + TopicSent = 3000, + /// + /// Not in use anymore + /// + [Obsolete("Not in use anymore", true)] + TopicFailed = 3100, + /// + /// Info: When the has been generated + /// + CommsKeySet = 3200, + /// + /// Not in use anymore + /// + [Obsolete("Not in use anymore", true)] + NudgeStartFail = 3300, + /// + /// Not in use anymore + /// + [Obsolete("Not in use anymore", true)] + NudgeCrash = 3400, + /// + /// Info: Successful completion of a operation + /// + RepoClone = 3500, + /// + /// Warning: An error occurred during a operation + /// + RepoCloneFail = 3600, + /// + /// Info: Successful completion of a operation + /// + RepoCheckout = 3700, + /// + /// Warning: An error occurred during a operation + /// + RepoCheckoutFail = 3800, + /// + /// Info: Successful completion of a operation with a parameter + /// + RepoHardUpdate = 3900, + /// + /// Warning: An error occurred during a operation with a parameter + /// + RepoHardUpdateFail = 4000, + /// + /// Info: Successful completion of a operation with a parameter + /// + RepoMergeUpdate = 4100, + /// + /// Warning: An error occurred during a operation with a parameter + /// + RepoMergeUpdateFail = 4200, + /// + /// Info: A backup tag of the repository in it's current state was successfully created + /// + RepoBackupTag = 4300, + /// + /// Warning: A backup tag of the repository in it's current state failed to be created + /// + RepoBackupTagFail = 4400, + /// + /// Info: Successful completion of a operation with a parameter + /// + RepoResetTracked = 4500, + /// + /// Warning: An error occurred during a operation with a parameter + /// + RepoResetTrackedFail = 4600, + /// + /// Info: Successful completion of a operation with a parameter + /// + RepoReset = 4700, + /// + /// Warning: An error occurred during a operation with a parameter + /// + RepoResetFail = 4800, + /// + /// Error: Failed to update or delete the testmerged PR list + /// + RepoPRListError = 4900, + /// + /// Info: Successful completion of a operation + /// + RepoPRMerge = 5000, + /// + /// Warning: An error occurred during a operation + /// + RepoPRMergeFail = 5100, + /// + /// Info: Successfully committed the paths specified in the synchronize_directories field of the 's TGS3.json + /// + RepoCommit = 5200, + /// + /// Warning: An error occurred while committing the paths specified in the synchronize_directories field of the 's TGS3.json + /// + RepoCommitFail = 5300, + /// + /// Info: Successful completion of a operation + /// + RepoPush = 5400, + /// + /// Warning: An error occurred during a operation + /// + RepoPushFail = 5500, + /// + /// Info: Successful completion of a operation + /// + RepoChangelog = 5600, + /// + /// Warning: An error occurred during a operation + /// + RepoChangelogFail = 5700, + /// + /// Info: When the dll is updated for the + /// + InterfaceDLLUpdated = 5800, + /// + /// Error: An error occurred while updating the dll for the + /// + InterfaceDLLUpdateFail = 5900, + /// + /// Error: An error occurred while starting the + /// + InstanceInitializationFailure = 6000, + /// + /// Error: When an exception occurs while the is stopping + /// + ServiceShutdownFail = 6100, + /// + /// Info: When the reboots in BYOND + /// + WorldReboot = 6200, + /// + /// Info: When the output of is applied to the live + /// + ServerUpdateApplied = 6300, + /// + /// Warning: When an exception occurs during a operation + /// + ChatBroadcastFail = 6400, + /// + /// Not in use anymore + /// + [Obsolete("Not in use anymore", true)] + IRCLogModes = 6500, + /// + /// Warning: When the Repository submodule handler has to reclone a submodule entirely. This is a long operation and is due to an upstream bug + /// + SubmoduleReclone = 6600, + /// + /// This event is of type or . It occurs when a user different from the previous one tries and either succeeds or fails to access a . DreamDaemon itself successfully accessing will not trigger this + /// + Authentication = 6700, + /// + /// Info: When a Preaction event successfully completes + /// + PreactionEvent = 6800, + /// + /// Warning: When a Preaction event fails + /// + PreactionFail = 6900, + /// + /// Warning: When a command from DreamDaemon fails + /// + InteropCallException = 7000, + /// + /// Warning: When the running DreamDaemon code does not have the correct API to talk to the + /// + APIVersionMismatch = 7100, + /// + /// Warning: If a .dll in the repository's TGS3.json could not be found. Error: If a symlink could not be established to a static .dll or directory + /// + RepoConfigurationFail = 7200, + /// + /// Info: Successfully read of a static path. Warning: An error occurred during a read of a static path + /// + StaticRead = 7300, + /// + /// Info: Successfully write of a static path. Warning: An error occurred during a write of a static path + /// + StaticWrite = 7400, + /// + /// Info: Successfully delete of a static path. Warning: An error occurred during a delete of a static path + /// + StaticDelete = 7500, + } +} diff --git a/TGServerService/InstanceConfig.cs b/TGServerService/InstanceConfig.cs index 3875bca00f..22239f645e 100644 --- a/TGServerService/InstanceConfig.cs +++ b/TGServerService/InstanceConfig.cs @@ -11,11 +11,11 @@ namespace TGServerService [ScriptIgnore] const string JSONFilename = "Instance.json"; [ScriptIgnore] - const ulong CurrentVersion = 0; //Literally any time you add/deprecated a field, this number needs to be bumped + protected const ulong CurrentVersion = 0; //Literally any time you add/deprecated a field, this number needs to be bumped [ScriptIgnore] - string InstanceDir; + protected string InstanceDir; - public ulong Version { get; private set; } = CurrentVersion; + public ulong Version { get; protected set; } = CurrentVersion; public Guid ID { get; private set; } = Guid.NewGuid(); public string Name { get; set; } = "TG Station Server"; @@ -24,7 +24,7 @@ namespace TGServerService public string ProjectName { get; set; } = "tgstation"; public ushort Port { get; set; } = 1337; - public TGDreamDaemonSecurity Security { get; set; } = TGDreamDaemonSecurity.Trusted; + public DreamDaemonSecurity Security { get; set; } = DreamDaemonSecurity.Trusted; public bool Autostart { get; set; } = false; public bool Webclient { get; set; } = false; @@ -54,33 +54,8 @@ namespace TGServerService var configtext = File.ReadAllText(Path.Combine(path, JSONFilename)); var res = new JavaScriptSerializer().Deserialize(configtext); res.InstanceDir = path; - for (; res.Version < CurrentVersion; ++res.Version) - res.Migrate(); + res.MigrateToCurrentVersion(); return res; } - - public void ConvertNETConfigToInstanceConfig() - { - var Config = Properties.Settings.Default; - new InstanceConfig() - { - InstanceDir = (string)Config.GetPreviousVersion("ServerDirectory"), - ProjectName = (string)Config.GetPreviousVersion("ProjectName"), - Port = (ushort)Config.GetPreviousVersion("ServerPort"), - CommitterName = (string)Config.GetPreviousVersion("CommitterName"), - CommitterEmail = (string)Config.GetPreviousVersion("CommitterEmail"), - Security = (TGDreamDaemonSecurity)Config.GetPreviousVersion("ServerSecurity"), - Autostart = (bool)Config.GetPreviousVersion("DDAutoStart"), - ChatProviderData = (string)Config.GetPreviousVersion("ChatProviderData"), - ChatProviderEntropy = (string)Config.GetPreviousVersion("ChatProviderEntropy"), - ReattachRequired = (bool)Config.GetPreviousVersion("ReattachToDD"), - ReattachProcessID = (int)Config.GetPreviousVersion("ReattachPID"), - ReattachPort = (ushort)Config.GetPreviousVersion("ReattachPort"), - ReattachCommsKey = (string)Config.GetPreviousVersion("ReattachCommsKey"), - ReattachAPIVersion = (string)Config.GetPreviousVersion("ReattachAPIVersion"), - AutoUpdateInterval = (ulong)Config.GetPreviousVersion("AutoUpdateInterval"), - AuthorizedUserGroupSID = (string)Config.GetPreviousVersion("AuthorizedGroupSID") - }.Save(); - } } } diff --git a/TGServerService/MessageType.cs b/TGServerService/MessageType.cs new file mode 100644 index 0000000000..8db0cf2298 --- /dev/null +++ b/TGServerService/MessageType.cs @@ -0,0 +1,28 @@ +using System; + +namespace TGServerService +{ + /// + /// Type of chat message, these may be OR'd together + /// + [Flags] + enum MessageType + { + /// + /// 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/PreactionHandler.cs b/TGServerService/PreactionHandler.cs deleted file mode 100644 index 802a3da52a..0000000000 --- a/TGServerService/PreactionHandler.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; - -namespace TGServerService -{ - // Some useful functions for triggering pre action events - partial class TGStationServer - { - const string EventFolder = "EventHandlers/"; - - string GetPath(string eventName) - { - return string.Format("{0}{1}.bat", EventFolder, eventName); - } - - bool EventHandlerExists(string eventName) - { - return File.Exists(GetPath(eventName)); - } - - bool HandleEvent(string eventName) - { - if (!EventHandlerExists(eventName)) - { - // We don't need a handler, so let's just fail silently. - return true; - } - - var process = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = GetPath(eventName), - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - } - }; - process.Start(); - process.WaitForExit(); - - var stdout = process.StandardOutput.ReadToEnd(); - var stderr = process.StandardError.ReadToEnd(); - - TGServerService.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 - ); - - return process.ExitCode == 0 ? true : false; - } - - public bool PrecompileHook() - { - return HandleEvent("precompile"); - } - - public bool PostcompileHook() - { - return HandleEvent("postcompile"); - } - } -} diff --git a/TGServerService/ProcessExtension.cs b/TGServerService/ProcessExtension.cs index 3fc590b89b..74758b40f3 100644 --- a/TGServerService/ProcessExtension.cs +++ b/TGServerService/ProcessExtension.cs @@ -4,31 +4,43 @@ using System.Runtime.InteropServices; namespace TGServerService { - //lightly massaged code from https://stackoverflow.com/a/13109774 + /// + /// Helpers to ing and a . Lightly massaged code from https://stackoverflow.com/a/13109774. Documentation linked from MSDN on 20/10/2017 + /// public static class ProcessExtension { + /// + /// https://msdn.microsoft.com/en-us/library/windows/desktop/ms686769(v=vs.85).aspx + /// enum ThreadAccess : int { - TERMINATE = (0x0001), SUSPEND_RESUME = (0x0002), - GET_CONTEXT = (0x0008), - SET_CONTEXT = (0x0010), - SET_INFORMATION = (0x0020), - QUERY_INFORMATION = (0x0040), - SET_THREAD_TOKEN = (0x0080), - IMPERSONATE = (0x0100), - DIRECT_IMPERSONATION = (0x0200) } - + /// + /// https://msdn.microsoft.com/en-us/library/windows/desktop/ms684335(v=vs.85).aspx + /// [DllImport("kernel32.dll")] static extern IntPtr OpenThread(ThreadAccess dwDesiredAccess, bool bInheritHandle, uint dwThreadId); + /// + /// https://msdn.microsoft.com/en-us/library/windows/desktop/ms724211(v=vs.85).aspx + /// [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr hObject); + /// + /// https://msdn.microsoft.com/en-us/library/windows/desktop/ms686345(v=vs.85).aspx + /// [DllImport("kernel32.dll")] static extern uint SuspendThread(IntPtr hThread); + /// + /// https://msdn.microsoft.com/en-us/library/windows/desktop/ms685086(v=vs.85).aspx + /// [DllImport("kernel32.dll")] static extern int ResumeThread(IntPtr hThread); + /// + /// Suspends all threads for a running + /// + /// The to suspend public static void Suspend(this Process process) { foreach (ProcessThread thread in process.Threads) @@ -40,6 +52,11 @@ namespace TGServerService CloseHandle(pOpenThread); } } + + /// + /// Resumes all threads for a running + /// + /// The to suspend public static void Resume(this Process process) { foreach (ProcessThread thread in process.Threads) diff --git a/TGServerService/Program.cs b/TGServerService/Program.cs index ef6ab4bfc1..30e08ba944 100644 --- a/TGServerService/Program.cs +++ b/TGServerService/Program.cs @@ -4,12 +4,19 @@ using System.IO; namespace TGServerService { - public static class Program + static class Program { - static void Main() => new TGServerService(); //wondows - - //Everything in this file is just generic helpers + /// + /// Entry point to the program + /// + static void Main() => new Service(); + /// + /// Copy a file from to , but first ensure the destination directory exists + /// + /// The source file + /// The destination file + /// If , will overwrite if it is a file. Otherwise, if exists, an exception will be thrown public static void CopyFileForceDirectories(string source, string dest, bool overwrite) { try @@ -21,6 +28,12 @@ namespace TGServerService } //http://stackoverflow.com/questions/1701457/directory-delete-doesnt-work-access-denied-error-but-under-windows-explorer-it + /// + /// Recursive directory deleter + /// + /// The directory to delete + /// If , an empty will remain instead of being deleted fully. Incompatible with + /// If any files or directories in the root level of match anything in this of s, they won't be deleted. Incompatible with public static void DeleteDirectory(string path, bool ContentsOnly = false, IList excludeRoot = null) { var di = new DirectoryInfo(path); @@ -43,6 +56,11 @@ namespace TGServerService } } + /// + /// Recursively empty a directory + /// + /// of the directory to empty + /// Lowercase file and directory names to skip while emptying this level. Not passed forward static void NormalizeAndDelete(DirectoryInfo dir, IList excludeRoot) { foreach (var subDir in dir.GetDirectories()) @@ -61,8 +79,35 @@ namespace TGServerService } } + /// + /// Recusively copy a directory + /// + /// The directory to copy + /// The destination directory + /// List of files and directories to ignore while copying + /// If no error will be thrown if does not exist public static void CopyDirectory(string sourceDirName, string destDirName, IList ignore = null, bool ignoreIfNotExists = false) { + IList realIgnore; + if (ignore != null) + { + realIgnore = new List(); + foreach (var I in ignore) + realIgnore.Add(I.ToLower()); + } + else + realIgnore = null; + CopyDirectoryImpl(sourceDirName, destDirName, realIgnore, ignoreIfNotExists); + } + + /// + /// Recusively copy a directory + /// + /// The directory to copy + /// The destination directory + /// List of lowercase files and directories to ignore while copying + /// If no error will be thrown if does not exist + static void CopyDirectoryImpl(string sourceDirName, string destDirName, IList ignore, bool ignoreIfNotExists) { // If the destination directory doesn't exist, create it. if (!Directory.Exists(destDirName)) { @@ -86,7 +131,7 @@ namespace TGServerService FileInfo[] files = dir.GetFiles(); foreach (FileInfo file in files) { - if (ignore != null && ignore.Contains(file.Name)) + if (ignore != null && ignore.Contains(file.Name.ToLower())) continue; string temppath = Path.Combine(destDirName, file.Name); file.CopyTo(temppath, true); @@ -95,11 +140,21 @@ namespace TGServerService // copy them and their contents to new location. foreach (DirectoryInfo subdir in dirs) { - if (ignore != null && ignore.Contains(subdir.Name)) + if (ignore != null && ignore.Contains(subdir.Name.ToLower())) continue; string temppath = Path.Combine(destDirName, subdir.Name); - CopyDirectory(subdir.FullName, temppath, ignore); + CopyDirectoryImpl(subdir.FullName, temppath, ignore, false); } } + + /// + /// Properly escapes characters for a BYOND Topic() packet. See http://www.byond.com/docs/ref/info.html#/proc/list2params + /// + /// The to sanitize + /// The sanitized string + 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..82180bdcf6 100644 --- a/TGServerService/ProjectInstaller.Designer.cs +++ b/TGServerService/ProjectInstaller.Designer.cs @@ -1,6 +1,6 @@ -namespace ServerService +namespace TGServerService { - partial class ProjectInstaller + public partial class ProjectInstaller { /// /// Required designer variable. @@ -54,7 +54,13 @@ #endregion + /// + /// The project's + /// private System.ServiceProcess.ServiceProcessInstaller serviceProcessInstaller1; + /// + /// The project's + /// private System.ServiceProcess.ServiceInstaller serviceInstaller1; } } \ No newline at end of file diff --git a/TGServerService/ProjectInstaller.cs b/TGServerService/ProjectInstaller.cs index 3c405c0328..12a82fa238 100644 --- a/TGServerService/ProjectInstaller.cs +++ b/TGServerService/ProjectInstaller.cs @@ -1,12 +1,17 @@ using System.ComponentModel; using System.Configuration.Install; -using System.ServiceProcess; -namespace ServerService +namespace TGServerService { + /// + /// This tells the .msi there is a Windows in this that needs installation + /// [RunInstaller(true)] public partial class ProjectInstaller : Installer { + /// + /// Construct a + /// public ProjectInstaller() { InitializeComponent(); diff --git a/TGServerService/Properties/Settings.Designer.cs b/TGServerService/Properties/Settings.Designer.cs index a0547ffe55..1bbb0d95f0 100644 --- a/TGServerService/Properties/Settings.Designer.cs +++ b/TGServerService/Properties/Settings.Designer.cs @@ -81,17 +81,5 @@ namespace TGServerService.Properties { this["InstancePaths"] = value; } } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("False")] - public bool ConvertedFromNETConfig { - get { - return ((bool)(this["ConvertedFromNETConfig"])); - } - set { - this["ConvertedFromNETConfig"] = value; - } - } } } diff --git a/TGServerService/Properties/Settings.settings b/TGServerService/Properties/Settings.settings index 4372a26778..447b6c7ee3 100644 --- a/TGServerService/Properties/Settings.settings +++ b/TGServerService/Properties/Settings.settings @@ -17,8 +17,5 @@ - - False - \ No newline at end of file diff --git a/TGServerService/RepoConfig.cs b/TGServerService/RepoConfig.cs new file mode 100644 index 0000000000..d406e658ff --- /dev/null +++ b/TGServerService/RepoConfig.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Web.Script.Serialization; + +namespace TGServerService +{ + /// + /// Repository specific information for a + /// + sealed class RepoConfig : IEquatable + { + /// + /// If this json is setup to support + /// + public readonly bool ChangelogSupport; + /// + /// Path to the repository's changelog generator script + /// + public readonly string PathToChangelogPy; + /// + /// Arguments for the changelog gennerator script + /// + public readonly string ChangelogPyArguments; + /// + /// List of python pip dependencies for the changelog generator script + /// + public readonly IList PipDependancies = new List(); + /// + /// Paths to commit and push to the remote repository + /// + public readonly IList PathsToStage = new List(); + /// + /// Directory's whose contents should not be touched when the updates + /// + public readonly IList StaticDirectoryPaths = new List(); + /// + /// DLL's used by DreamDaemon call()() operations. Must be handled as symlinks to avoid lockups during update operations + /// + public readonly IList DLLPaths = new List(); + + /// + /// Construct a RepoConfig + /// + /// Path to the config JSON to use + public RepoConfig(string path) + { + 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 { } + } + + /// + /// Convert an array of s to a of s + /// + /// The array to convert + /// + 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); + } + + /// + /// Check if two s have the same contents + /// + /// The first + /// The second + /// if the s match, otherwise + 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); + } + } +} diff --git a/TGServerService/Administration.cs b/TGServerService/ServerInstance/Administration.cs similarity index 64% rename from TGServerService/Administration.cs rename to TGServerService/ServerInstance/Administration.cs index 2c30cc178e..5ce99489de 100644 --- a/TGServerService/Administration.cs +++ b/TGServerService/ServerInstance/Administration.cs @@ -4,17 +4,30 @@ 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 + sealed partial class ServerInstance : ServiceAuthorizationManager, ITGAdministration { + /// + /// The of the Windows group authorized to access the + /// SecurityIdentifier TheDroidsWereLookingFor; + /// + /// Used for multithreading safety + /// object authLock = new object(); - string LastSeenUser = null; + /// + /// The of the last to attempt to access the + /// + string LastSeenUser; + /// + /// The of the account the is running as + /// readonly SecurityIdentifier ServiceSID = WindowsIdentity.GetCurrent().User; /// @@ -47,6 +60,11 @@ namespace TGServerService return FindTheDroidsWereLookingFor(groupName); } + /// + /// Set based off either an ed name or a string from the config + /// + /// The name of the group to search for + /// The name of the group allowed to access the if it could be found, otherwise string FindTheDroidsWereLookingFor(string search = null) { //find the group that is authorized to use the tools @@ -71,9 +89,12 @@ namespace TGServerService } return gp.Name; } - - //This function checks for authorization whenever an API call is made - //This does NOT validate the windows account, that is done when the user connects internally + + /// + /// Called by WCF whenever a component call is made. Checks to see that the supplied user account has access to the requested component + /// + /// Various parameters about the operation supplied by WCF + /// if the supplied user account may use the requested component, otherwise protected override bool CheckAccessCore(OperationContext operationContext) { var contract = operationContext.EndpointDispatcher.ContractName; @@ -83,8 +104,14 @@ namespace TGServerService var windowsIdent = operationContext.ServiceSecurityContext.WindowsIdentity; - if (contract == typeof(ITGInterop).Name) //only allow the same user the service is running as to use interop, because that's what DD is running as - return windowsIdent.User == ServiceSID; + if (contract == typeof(ITGInterop).Name) + { + //only allow the same user the service is running as to use interop, because that's what DD is running as, and don't spam the logs with it unless it fails + var result = windowsIdent.User == ServiceSID; + if(!result) + Service.WriteAccess(windowsIdent.Name, false); + return result; + } var wp = new WindowsPrincipal(windowsIdent); //first allow admins @@ -100,7 +127,7 @@ namespace TGServerService if (LastSeenUser != user) { LastSeenUser = user; - TGServerService.WriteAccess(user, authSuccess); + Service.WriteAccess(user, authSuccess); } } return authSuccess; @@ -120,7 +147,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/ServerInstance/Byond.cs b/TGServerService/ServerInstance/Byond.cs new file mode 100644 index 0000000000..9a3715ff99 --- /dev/null +++ b/TGServerService/ServerInstance/Byond.cs @@ -0,0 +1,350 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading; +using TGServiceInterface; +using TGServiceInterface.Components; + +namespace TGServerService +{ + sealed partial class ServerInstance : ITGByond + { + /// + /// The instance directory to store the BYOND installation + /// + const string ByondDirectory = "BYOND"; + /// + /// The instance directory to use when updating the BYOND installation + /// + const string StagingDirectory = "BYOND_staged"; + /// + /// Path to the actual BYOND installation within the + /// + const string StagingDirectoryInner = StagingDirectory + "/byond"; + /// + /// The path in the instance directory to store the downloaded BYOND revision + /// + const string RevisionDownloadPath = "BYONDRevision.zip"; + /// + /// The location of the BYOND version data of an installation + /// + const string VersionFile = "/byond_version.dat"; + /// + /// The URL format string for getting BYOND version {0}.{1} zipfile + /// + const string ByondRevisionsURL = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip"; + /// + /// The URL for getting the latest BYOND version zipfile + /// + const string ByondLatestURL = "https://secure.byond.com/download/build/LATEST/"; + /// + /// The instance directory to modify the BYOND cfg before installation + /// + const string ByondConfigDir = StagingDirectory + "/BYOND/cfg"; + /// + /// BYOND's DreamDaemon config file in the cfg modification directory + /// + const string ByondDDConfig = ByondConfigDir + "/daemon.txt"; + /// + /// Setting to add to to suppress an invisible user prompt for running a trusted mode .dmb + /// + const string ByondNoPromptTrustedMode = "trusted-check 0"; + + /// + /// The status of the BYOND updater + /// + ByondStatus updateStat = ByondStatus.Idle; + + /// + /// Used for multithreading safety + /// + object ByondLock = new object(); + /// + /// The last error the BYOND updater encountered + /// + string lastError; + /// + /// Thread used for staging BYOND revisions + /// + Thread RevisionStaging; + + /// + /// Called when the is setup. Prepares the BYOND updater + /// + void InitByond() + { + CleanByondStaging(); + } + + /// + /// Cleans the BYOND staging directory + /// + void CleanByondStaging() + { + //linger not + if (File.Exists(RevisionDownloadPath)) + File.Delete(RevisionDownloadPath); + Program.DeleteDirectory(StagingDirectory); + } + + /// + /// Called when the is shutdown + /// + void DisposeByond() + { + lock (ByondLock) + { + if (RevisionStaging != null) + RevisionStaging.Abort(); + CleanByondStaging(); + } + } + + /// + /// Checks if the updater is considered busy + /// + /// if the updater is considered busy, otherwise + bool BusyCheck() + { + lock (ByondLock) + switch (updateStat) + { + default: + case ByondStatus.Starting: + case ByondStatus.Downloading: + case ByondStatus.Staging: + case ByondStatus.Updating: + return true; + case ByondStatus.Idle: + case ByondStatus.Staged: + return false; + } + } + + /// + public ByondStatus CurrentStatus() + { + lock (ByondLock) + { + return updateStat; + } + } + + /// + public string GetError() + { + lock (ByondLock) + { + var error = lastError; + lastError = null; + return error; + } + } + + /// + public string GetVersion(ByondVersion type) + { + try + { + lock (ByondLock) + { + if (type == ByondVersion.Latest) + { + //get the latest version from the website + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(ByondLatestURL); + var results = new List(); + using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) + { + using (StreamReader reader = new StreamReader(response.GetResponseStream())) + { + string html = reader.ReadToEnd(); + + Regex regex = new Regex("\\\"([^\"]*)\\\""); + MatchCollection matches = regex.Matches(html); + foreach (Match match in matches) + if (match.Success && match.Value.Contains("_byond.exe")) + results.Add(match.Value.Replace("\"", "").Replace("_byond.exe", "")); + } + } + results.Sort(); + results.Reverse(); + return results.Count > 0 ? results[0] : null; + } + else + { + string DirToUse = type == ByondVersion.Staged ? StagingDirectoryInner : ByondDirectory; + if (Directory.Exists(DirToUse)) + { + string file = DirToUse + VersionFile; + if (File.Exists(file)) + return File.ReadAllText(file); + } + } + return null; + } + } + catch (Exception e) + { + return "Error: " + e.ToString(); + } + } + + /// + /// Downloads and unzips a BYOND revision. Calls afterwards if the isn't running, otherwise, calls . Sets on failure + /// + /// Stringified BYOND revision + public void UpdateToVersionImpl(object param) + { + lock (ByondLock) { + if (updateStat != ByondStatus.Starting) + return; + updateStat = ByondStatus.Downloading; + } + + try + { + CleanByondStaging(); + + var vi = ((string)param).Split('.'); + var major = Convert.ToInt32(vi[0]); + var minor = Convert.ToInt32(vi[1]); + using (var client = new WebClient()) + { + SendMessage(String.Format("BYOND: Updating to version {0}.{1}...", major, minor), MessageType.DeveloperInfo); + + //DOWNLOADING + + try + { + client.DownloadFile(String.Format(ByondRevisionsURL, major, minor), RevisionDownloadPath); + } + catch + { + SendMessage("BYOND: Update download failed. Does the specified version exist?", MessageType.DeveloperInfo); + lastError = String.Format("Download of BYOND version {0}.{1} failed! Does it exist?", major, minor); + Service.WriteWarning(String.Format("Failed to update BYOND to version {0}.{1}!", major, minor), EventID.BYONDUpdateFail); + lock (ByondLock) + { + updateStat = ByondStatus.Idle; + } + return; + } + } + lock (ByondLock) + { + updateStat = ByondStatus.Staging; + } + + //STAGING + + ZipFile.ExtractToDirectory(RevisionDownloadPath, StagingDirectory); + lock (ByondLock) + { + File.WriteAllText(StagingDirectoryInner + VersionFile, String.Format("{0}.{1}", major, minor)); + //IMPORTANT: SET THE BYOND CONFIG TO NOT PROMPT FOR TRUSTED MODE REEE + Directory.CreateDirectory(ByondConfigDir); + File.WriteAllText(ByondDDConfig, ByondNoPromptTrustedMode); + } + File.Delete(RevisionDownloadPath); + + lock (ByondLock) + { + updateStat = ByondStatus.Staged; + } + + switch (DaemonStatus()) + { + case DreamDaemonStatus.Offline: + if(ApplyStagedUpdate()) + lastError = null; + else + lastError = "Failed to apply update!"; + break; + default: + RequestRestart(); + lastError = "Update staged. Awaiting server restart..."; + SendMessage(String.Format("BYOND: Staging complete. Awaiting server restart...", major, minor), MessageType.DeveloperInfo); + Service.WriteInfo(String.Format("BYOND update {0}.{1} staged", major, minor), EventID.BYONDUpdateStaged); + break; + } + } + catch (ThreadAbortException) + { + return; + } + catch (Exception e) + { + Service.WriteError("Revision staging errror: " + e.ToString(), EventID.BYONDUpdateFail); + lock (ByondLock) + { + updateStat = ByondStatus.Idle; + lastError = e.ToString(); + RevisionStaging = null; + } + } + } + /// + public bool UpdateToVersion(int major, int minor) + { + lock (ByondLock) + { + if (!BusyCheck()) + { + updateStat = ByondStatus.Starting; + RevisionStaging = new Thread(new ParameterizedThreadStart(UpdateToVersionImpl)) + { + IsBackground = true //don't slow me down + }; + RevisionStaging.Start(String.Format("{0}.{1}", major, minor)); + return true; + } + return false; + } + } + + /// + /// Attempts to move the staged update from to . Sets on failure + /// + /// on success, on failure + bool ApplyStagedUpdate() + { + lock (CompilerLock) + { + if (compilerCurrentStatus == CompilerStatus.Compiling) + return false; + lock (ByondLock) + { + if (updateStat != ByondStatus.Staged) + return false; + updateStat = ByondStatus.Updating; + } + try + { + Program.DeleteDirectory(ByondDirectory); + Directory.Move(StagingDirectoryInner, ByondDirectory); + Program.DeleteDirectory(StagingDirectory); + lastError = null; + SendMessage("BYOND: Update completed!", MessageType.DeveloperInfo); + 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!", MessageType.DeveloperInfo); + Service.WriteError("BYOND update failed! Error: " + e.ToString(), EventID.BYONDUpdateFail); + return false; + } + finally + { + lock(ByondLock) { + updateStat = ByondStatus.Idle; + } + } + } + } + } +} diff --git a/TGServerService/ServerInstance/Chat.cs b/TGServerService/ServerInstance/Chat.cs new file mode 100644 index 0000000000..c4df5cf4cf --- /dev/null +++ b/TGServerService/ServerInstance/Chat.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using TGServerService.ChatCommands; +using TGServerService.ChatProviders; +using TGServiceInterface; +using TGServiceInterface.Components; + +namespace TGServerService +{ + sealed partial class ServerInstance : ITGChat + { + /// + /// List of s for the + /// + IList ChatProviders; + /// + /// Used for multithreading safety + /// + object ChatLock = new object(); + + /// + /// Set up the for the + /// + public void InitChat() + { + var infos = InitProviderInfos(); + ChatProviders = new List(infos.Count); + foreach (var info in infos) + { + IChatProvider 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); + } + } + + /// + /// Implementation of that recieves messages from all channels of all connected + /// + /// The that heard the + /// The user who wrote the + /// The channel the is from + /// The recieved message + /// if is considered a chat admin, otherwise + /// if is an admin channel, otherwise + private void ChatProvider_OnChatMessage(IChatProvider 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); + } + + /// + /// Properly shuts down all + /// + 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); + + 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; + } + + /// + /// Returns a list of s loaded from the config or the defaults if none are set + /// + /// A list of s loaded from the config or the defaults if none are set + IList InitProviderInfos() + { + lock (ChatLock) + { + 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 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 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. Checked every time DreamDaemon reboots + /// + void ChatConnectivityCheck() + { + foreach (ChatProvider I in Enum.GetValues(typeof(ChatProvider))) + if(!Connected(I)) + Reconnect(I); + } + + /// + 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, MessageType 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 76% rename from TGServerService/Compiler.cs rename to TGServerService/ServerInstance/Compiler.cs index 71c78187c4..3f77b7b1a8 100644 --- a/TGServerService/Compiler.cs +++ b/TGServerService/ServerInstance/Compiler.cs @@ -1,633 +1,635 @@ -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 +{ + sealed 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 CompilerStatus GetStatus() + { + lock (CompilerLock) + { + return compilerCurrentStatus; + } + } + + /// + 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 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...", MessageType.DeveloperInfo); + CleanGameFolder(); + Program.DeleteDirectory(GameDir); + + Directory.CreateDirectory(GameDirA); + Directory.CreateDirectory(GameDirB); + + var Config = GetCachedRepoConfig(); + + 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!", MessageType.DeveloperInfo); + Service.WriteError("Compiler Initialization Error: " + e.ToString(), EventID.DMInitializeCrash); + 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 + "/" + Config.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!", MessageType.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...", MessageType.DeveloperInfo); + + resurrectee = GetStagingDir(); + + var Config = GetCachedRepoConfig(); + 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, MessageType.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, MessageType.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!", MessageType.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!", MessageType.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!", MessageType.DeveloperInfo); + Service.WriteInfo("Compilation cancelled", EventID.DMCompileCancel); + } + } + } + } + //kicks off the compiler thread + /// + 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 string ProjectName() + { + lock (CompilerLock) + { + return Config.ProjectName; + } + } + + /// + public void SetProjectName(string projectName) + { + lock (CompilerLock) + { + Config.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 78% rename from TGServerService/Config.cs rename to TGServerService/ServerInstance/Config.cs index 188a2c838d..257952cac2 100644 --- a/TGServerService/Config.cs +++ b/TGServerService/ServerInstance/Config.cs @@ -1,39 +1,41 @@ 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 + sealed partial class ServerInstance : ITGConfig { + /// + /// Used for multithreading safety + /// object configLock = new object(); //for atomic reads/writes - //public api + /// public string ServerDirectory() { return Environment.CurrentDirectory; } - //public api + /// [OperationBehavior(Impersonation = ImpersonationOption.Required)] public string ReadText(string staticRelativePath, bool repo, out string error, out bool unauthorized) { + string path = null; try { var configDir = repo ? RepoPath : StaticDirs; - var path = configDir + '/' + staticRelativePath; //do not use path.combine or it will try and take the root + path = configDir + '/' + staticRelativePath; //do not use path.combine or it will try and take the root lock (configLock) { var di1 = new DirectoryInfo(configDir); if (repo) { //ensure we aren't trying to read anything outside the static dirs - var Config = new RepoConfig(false); + var Config = GetCachedRepoConfig(); if (Config == null) { error = "Unable to load static directory configuration"; @@ -78,8 +80,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; @@ -95,16 +97,20 @@ namespace TGServerService catch (Exception e) { error = e.ToString(); + Service.CancelImpersonation(); + Service.WriteWarning(String.Format("Read of {0} failed! Error: {1}", path, e.ToString()), EventID.StaticRead); unauthorized = false; return null; } } + + /// [OperationBehavior(Impersonation = ImpersonationOption.Required)] public string WriteText(string staticRelativePath, string data, out bool unauthorized) { + var path = StaticDirs + '/' + staticRelativePath; //do not use path.combine or it will try and take the root try { - var path = StaticDirs + '/' + staticRelativePath; //do not use path.combine or it will try and take the root lock (configLock) { var di1 = new DirectoryInfo(StaticDirs); @@ -130,8 +136,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; } @@ -145,15 +151,18 @@ namespace TGServerService catch (Exception e) { unauthorized = false; + Service.CancelImpersonation(); + Service.WriteWarning(String.Format("Write of {0} failed! Error: {1}", path, e.ToString()), EventID.StaticRead); return e.ToString(); } } + /// [OperationBehavior(Impersonation = ImpersonationOption.Required)] public string DeleteFile(string staticRelativePath, out bool unauthorized) { + var path = StaticDirs + '/' + staticRelativePath; //do not use path.combine or it will try and take the root try { - var path = StaticDirs + '/' + staticRelativePath; //do not use path.combine or it will try and take the root lock (configLock) { var di1 = new DirectoryInfo(StaticDirs); @@ -181,8 +190,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; } @@ -196,10 +205,13 @@ namespace TGServerService catch (Exception e) { unauthorized = false; + Service.CancelImpersonation(); + Service.WriteWarning(String.Format("Delete of {0} failed! Error: {1}", path, e.ToString()), EventID.StaticRead); return e.ToString(); } } + /// [OperationBehavior(Impersonation = ImpersonationOption.Required)] public IList ListStaticDirectory(string subDir, out string error, out bool unauthorized) { diff --git a/TGServerService/ServerInstance/DreamDaemon.cs b/TGServerService/ServerInstance/DreamDaemon.cs new file mode 100644 index 0000000000..35529dc2dd --- /dev/null +++ b/TGServerService/ServerInstance/DreamDaemon.cs @@ -0,0 +1,725 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +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, security, etc. It restarts the process when the world reboots + sealed partial class ServerInstance : ITGDreamDaemon + { + enum ShutdownRequestPhase + { + None, + Requested, + Pinged, + } + + /// + /// Directory for storing DreamDaemon diagnostic files + /// + const string DiagnosticsDir = "Diagnostics"; + /// + /// Directory for storing DreamDaemon ResourceUsage files + /// + const string ResourceDiagnosticsDir = DiagnosticsDir + "/Resources"; + /// + /// Time until DD is considered DOA on startup + /// + const int DDHangStartTime = 60; + /// + /// If DreamDaemon crashes before this time it is considered a bad startup + /// + const int DDBadStartTime = 10; + + /// + /// The DreamDaemon process + /// + Process Proc; + /// + /// CPU performance information for + /// + PerformanceCounter pcpu; + + /// + /// Used for multithreading safety + /// + object watchdogLock = new object(); + /// + /// The thread that monitors the status of DreamDaemon + /// + Thread DDWatchdog; + /// + /// The of + /// + DreamDaemonStatus currentStatus; + /// + /// Current logfile in use in + /// + string CurrentDDLog; + /// + /// Current port DreamDaemon is running on + /// + ushort currentPort = 0; + + /// + /// Used for multithreading safety + /// + object restartLock = new object(); + /// + /// Used to indicate if an intentional restart is in progress on the watchdog. Requires to access + /// + bool RestartInProgress = false; + /// + /// Used to indicate if an service restart is in progress on the watchdog. Requires to access + /// + bool ReattachInsteadOfRestart = false; + + /// + /// Current level of DreamDaemon + /// + DreamDaemonSecurity StartingSecurity; + + /// + /// Indicator of progress on a or operation + /// + ShutdownRequestPhase AwaitingShutdown; + + /// + /// Setup or reattach the watchdog, depending on , and create the + /// + void InitDreamDaemon() + { + Directory.CreateDirectory(DiagnosticsDir); + Directory.CreateDirectory(ResourceDiagnosticsDir); + var Reattach = Config.ReattachRequired; + if (Reattach) + try + { + Proc = Process.GetProcessById(Config.ReattachProcessID); + if (Proc == null) + throw new Exception("GetProcessById returned null!"); + Service.WriteInfo("Reattached to running DD process!", EventID.DDReattachSuccess); + ThreadPool.QueueUserWorkItem(_ => + { + Thread.Sleep(5000); + SendMessage("DD: Update complete. Watch dog reactivated...", MessageType.WatchdogInfo); + }); + + //start wd + lock (restartLock) + { + RestartInProgress = true; + ReattachInsteadOfRestart = true; + } + currentPort = Config.ReattachPort; + serviceCommsKey = Config.ReattachCommsKey; + try + { + GameAPIVersion = new Version(Config.ReattachAPIVersion); + } + catch { } + currentStatus = DreamDaemonStatus.Online; + DDWatchdog = new Thread(new ThreadStart(Watchdog)); + DDWatchdog.Start(); + } + catch (Exception e) + { + Service.WriteError(String.Format("Failed to reattach to DreamDaemon! PID: {0}. Exception: {1}", Config.ReattachProcessID, e.ToString()), EventID.DDReattachFail); + } + finally + { + Config.ReattachRequired = false; + Config.Save(); + } + + if (Proc == null) + Proc = new Process(); + + Proc.StartInfo.FileName = ByondDirectory + "/bin/dreamdaemon.exe"; + Proc.StartInfo.UseShellExecute = false; + + if (Reattach) + return; + + //autostart the server + if (Config.Autostart) + //break this off so we don't hold up starting the service + ThreadPool.QueueUserWorkItem( _ => { Start(); }); + } + + /// + /// Either let go of for reattachment or terminate it, depending on + /// + void DisposeDreamDaemon() + { + var Detach = Config.ReattachRequired; + bool RenameLog = false; + if (DaemonStatus() == DreamDaemonStatus.Online) + { + if (!Detach) + { + WorldAnnounce("Server service stopped"); + Thread.Sleep(1000); + } + else + { + RenameLog = CurrentDDLog != null; + SendMessage("DD: Detaching watch dog for update!", MessageType.WatchdogInfo); + WriteCurrentDDLog("Service updating! Splitting diagnostics..."); + } + } + else if (Detach) + Config.ReattachRequired = false; + Stop(); + if(pcpu != null) + pcpu.Dispose(); + if (RenameLog) + try + { + File.Move(Path.Combine(ResourceDiagnosticsDir, CurrentDDLog), Path.Combine(ResourceDiagnosticsDir, "SU-" + CurrentDDLog)); + } + catch { } + } + + /// + public DreamDaemonStatus DaemonStatus() + { + lock (watchdogLock) + { + return currentStatus; + } + } + + /// + public void RequestRestart() + { + SendCommand(SCHardReboot); + } + + /// + public void RequestStop() + { + lock (watchdogLock) + { + if (currentStatus != DreamDaemonStatus.Online || AwaitingShutdown != ShutdownRequestPhase.None) + return; + AwaitingShutdown = ShutdownRequestPhase.Pinged; + } + SendCommand(SCGracefulShutdown); + } + + /// + public string Stop() + { + Thread t; + lock (watchdogLock) + { + t = DDWatchdog; + DDWatchdog = null; + } + if (t != null && t.IsAlive) + { + t.Abort(); + t.Join(); + return null; + } + else + return "Server not running"; + } + + /// + public void SetPort(ushort new_port) + { + lock (watchdogLock) + { + Config.Port = new_port; + RequestRestart(); + } + } + + //handle a kill request from the server + public void KillMe() + { + bool DoRestart; + lock (watchdogLock) + { + DoRestart = AwaitingShutdown == ShutdownRequestPhase.None; + if (!DoRestart) + AwaitingShutdown = ShutdownRequestPhase.Pinged; + } + //Do this is a seperate thread or we'll kill this thread in the middle of rebooting + if (DoRestart) + ThreadPool.QueueUserWorkItem(_ => { Restart(); }); + else + ThreadPool.QueueUserWorkItem(_ => { Stop(); }); + } + + /// + public string Restart() + { + if (DaemonStatus() == DreamDaemonStatus.Offline) + return Start(); + lock(restartLock) + { + if (RestartInProgress) + return "Restart already in progress"; + RestartInProgress = true; + } + SendMessage("DD: Hard restart triggered", MessageType.WatchdogInfo); + Stop(); + var res = Start(); + if(res != null) + lock(restartLock) + { + RestartInProgress = false; + } + return res; + } + + /// + /// Write a to the . A timestamp will be prepended to it + /// + /// The message to log + void WriteCurrentDDLog(string message) + { + lock (watchdogLock) + { + if (currentStatus != DreamDaemonStatus.Online || CurrentDDLog == null) + return; + File.AppendAllText(Path.Combine(ResourceDiagnosticsDir, CurrentDDLog), String.Format("[{0}]: {1}\n", DateTime.Now.ToLongTimeString(), message)); + } + } + + /// + /// Threaded loop that keeps DreamDaemon from unintentionally stopping + /// + void Watchdog() + { + try + { + lock (restartLock) + { + if (!RestartInProgress) + { + SendMessage("DD: Server started, watchdog active...", MessageType.WatchdogInfo); + Service.WriteInfo("Watchdog started", EventID.DDWatchdogStarted); + } + else + { + RestartInProgress = false; + if (!ReattachInsteadOfRestart) + Service.WriteInfo("Watchdog restarted", EventID.DDWatchdogRestarted); + else + ReattachInsteadOfRestart = false; + } + } + var retries = 0; + + var MemTrackTimer = new System.Timers.Timer + { + AutoReset = true, + Interval = 5000 //every 5 seconds + }; + MemTrackTimer.Elapsed += MemTrackTimer_Elapsed; + while (true) + { + var starttime = DateTime.Now; + + lock (watchdogLock) + { + if (AwaitingShutdown == ShutdownRequestPhase.Requested) + SendCommand(SCGracefulShutdown); + } + + //all good to go, let's start monitoring + var Now = DateTime.Now; + lock (watchdogLock) + { + CurrentDDLog = String.Format("{0} {1} Diagnostics.txt", Now.ToLongDateString(), Now.ToLongTimeString()).Replace(':', '-'); + WriteCurrentDDLog("Starting monitoring..."); + } + pcpu = new PerformanceCounter("Process", "% Processor Time", Proc.ProcessName, true); + MemTrackTimer.Start(); + try + { + Proc.WaitForExit(); + } + finally + { + lock (watchdogLock) //synchronize + { + MemTrackTimer.Stop(); + pcpu.Dispose(); + } + } + + WriteCurrentDDLog("Crash detected!"); + + lock (watchdogLock) + { + currentStatus = DreamDaemonStatus.HardRebooting; + currentPort = 0; + Proc.Close(); + + if (AwaitingShutdown == ShutdownRequestPhase.Pinged) + return; + var BadStart = (DateTime.Now - starttime).TotalSeconds < DDBadStartTime; + if (BadStart) + { + ++retries; + var sleep_time = (int)Math.Min(Math.Pow(2, retries), 3600); //max of one hour + SendMessage(String.Format("DD: Watchdog server startup failed! Retrying in {0} seconds...", sleep_time), MessageType.WatchdogInfo); + Thread.Sleep(sleep_time * 1000); + } + else + { + retries = 0; + var msg = "DD: DreamDaemon crashed! Watchdog rebooting DD..."; + SendMessage(msg, MessageType.WatchdogInfo); + Service.WriteWarning(msg, EventID.DDWatchdogRebootingServer); + } + } + + var res = StartImpl(true); + if (res != null) + throw new Exception("Hard restart failed: " + res); + } + } + catch (ThreadAbortException) + { + //No Mr bond, I expect you to die + try + { + if (!Config.ReattachRequired) + { + Proc.Kill(); + Proc.WaitForExit(); + } + else + { + Config.ReattachProcessID = Proc.Id; + Config.ReattachPort = currentPort; + Config.ReattachCommsKey = serviceCommsKey; + lock (restartLock) + { + RestartInProgress = true; + } + } + Proc.Close(); + } + catch + { } + } + catch (Exception e) + { + SendMessage("DD: Watchdog thread crashed!", MessageType.WatchdogInfo); + Service.WriteError("Watch dog thread crashed: " + e.ToString(), EventID.DDWatchdogCrash); + } + finally + { + lock (watchdogLock) + { + currentStatus = DreamDaemonStatus.Offline; + currentPort = 0; + AwaitingShutdown = ShutdownRequestPhase.None; + lock (restartLock) + { + if (!RestartInProgress) + { + if (!Config.ReattachRequired) + SendMessage("DD: Server stopped, watchdog exiting...", MessageType.WatchdogInfo); + Service.WriteInfo("Watch dog exited", EventID.DDWatchdogExit); + } + else + Service.WriteInfo("Watch dog restarting...", EventID.DDWatchdogRestart); + } + } + } + } + + /// + /// Called every five seconds while DreamDaemon is running to log it's current state to the + /// + /// The event sender, an instance of + /// The + private void MemTrackTimer_Elapsed(object sender, ElapsedEventArgs e) + { + ulong megamem; + float cputime; + lock (watchdogLock) + { + cputime = pcpu.NextValue(); + using (var pcm = new PerformanceCounter("Process", "Working Set - Private", Proc.ProcessName, true)) + megamem = Convert.ToUInt64(pcm.NextValue()) / 1024; + } + var PercentCpuTime = (int)Math.Round((Decimal)cputime); + WriteCurrentDDLog(String.Format("CPU: {1}% Memory: {0}MB", megamem, PercentCpuTime.ToString("D3"))); + } + + /// + public string CanStart() + { + if (GetVersion(ByondVersion.Installed) == null) + return "Byond is not installed!"; + var DMB = GameDirLive + "/" + Config.ProjectName + ".dmb"; + if (!File.Exists(DMB)) + return String.Format("Unable to find {0}!", DMB); + return null; + } + + /// + public string Start() + { + if (CurrentStatus() == ByondStatus.Staged) + { + //IMPORTANT: SLEEP FOR A MOMENT OR WONDOWS WON'T RELEASE THE FUCKING BYOND DLL HANDLES!!!! REEEEEEE + Thread.Sleep(3000); + ApplyStagedUpdate(); + } + lock (watchdogLock) + { + if (currentStatus != DreamDaemonStatus.Offline) + return "Server already running"; + var res = CanStart(); + if (res != null) + return res; + currentPort = 0; + currentStatus = DreamDaemonStatus.HardRebooting; + } + return StartImpl(false); + } + + /// + /// Translate the configured level into a byond command line param + /// + /// If bases it's result on , uses otherwise + /// "safe", "trusted", or "ultrasafe" depending on the it checks + string SecurityWord(bool starting = false) + { + var level = starting ? StartingSecurity : Config.Security; + switch (level) + { + case DreamDaemonSecurity.Safe: + return "safe"; + case DreamDaemonSecurity.Trusted: + return "trusted"; + case DreamDaemonSecurity.Ultrasafe: + return "ultrasafe"; + default: + throw new Exception(String.Format("Bad DreamDaemon security level: {0}", level)); + } + } + + /// + /// Copies from the program directory to the the directory + /// + /// If , overwrites the 's current interface .dll if it exists + void UpdateInterfaceDll(bool overwrite) + { + var FileExists = File.Exists(InterfaceDLLName); + if (FileExists && !overwrite) + return; + //Copy the interface dll to the static dir + var InterfacePath = Assembly.GetAssembly(typeof(DreamDaemonBridge)).Location; + try + { + if (FileExists) + { + var Old = File.ReadAllBytes(InterfaceDLLName); + var New = File.ReadAllBytes(InterfacePath); + if (Old.SequenceEqual(New)) + return; //no need + } + File.Copy(InterfacePath, InterfaceDLLName, overwrite); + Service.WriteInfo("Updated interface DLL", EventID.InterfaceDLLUpdated); + } + catch + { + try + { + //ok the things being stupid and hasn't released the dll yet, try ONCE more + Thread.Sleep(1000); + File.Copy(InterfacePath, InterfaceDLLName, overwrite); + } + catch (Exception e) + { + //intentionally using the fi + Service.WriteError("Failed to update interface DLL! Error: " + e.ToString(), EventID.InterfaceDLLUpdateFail); + } + } + } + + /// + /// Clears the current , calls with a parameter, and attempts to start the DreamDaemon + /// + /// If , sets to a new pointing to and starts it + /// on success, error message on failure + string StartImpl(bool watchdog) + { + try + { + lock (watchdogLock) + { + var res = CanStart(); + if (res != null) + return res; + + var DMB = GameDirLive + "/" + Config.ProjectName + ".dmb"; + + GenCommsKey(); + StartingSecurity = Config.Security; + Proc.StartInfo.Arguments = String.Format("{0} -port {1} {5}-close -verbose -params \"server_service={3}&server_service_version={4}\" -{2} -public", DMB, Config.Port, SecurityWord(), serviceCommsKey, Version(), Config.Webclient ? "-webclient " : ""); + UpdateInterfaceDll(true); + lock (topicLock) + { + GameAPIVersion = null; //needs updating + } + Proc.Start(); + + if (!Proc.WaitForInputIdle(DDHangStartTime * 1000)) + { + Proc.Kill(); + Proc.WaitForExit(); + Proc.Close(); + currentStatus = DreamDaemonStatus.Offline; + currentPort = 0; + return String.Format("Server start is taking more than {0}s! Aborting!", DDHangStartTime); + } + currentPort = Config.Port; + currentStatus = DreamDaemonStatus.Online; + if (!watchdog) + { + DDWatchdog = new Thread(new ThreadStart(Watchdog)); + DDWatchdog.Start(); + } + return null; + } + } + catch (Exception e) + { + currentStatus = DreamDaemonStatus.Offline; + return e.ToString(); + } + } + + /// + public DreamDaemonSecurity SecurityLevel() + { + lock (watchdogLock) + { + return Config.Security; + } + } + + /// + public bool SetSecurityLevel(DreamDaemonSecurity level) + { + bool needReboot; + lock (watchdogLock) + { + needReboot = Config.Security != level; + Config.Security = level; + } + if (needReboot) + RequestRestart(); + return DaemonStatus() != DreamDaemonStatus.Online; + } + + /// + public bool Autostart() + { + return Config.Autostart; + } + + /// + public void SetAutostart(bool on) + { + Config.Autostart = on; + } + + /// + public string StatusString(bool includeMetaInfo) + { + const string visSecStr = " (Sec: {0})"; + string res; + var ds = DaemonStatus(); + switch (ds) + { + case DreamDaemonStatus.Offline: + res = "OFFLINE"; + break; + case DreamDaemonStatus.HardRebooting: + res = "REBOOTING"; + break; + case DreamDaemonStatus.Online: + res = "ONLINE"; + if (includeMetaInfo) + { + string secandvis; + lock (watchdogLock) + { + secandvis = String.Format(visSecStr, SecurityWord(true)); + } + res += secandvis; + } + break; + default: + res = "NULL AND ERRORS"; + break; + } + if (includeMetaInfo && ds != DreamDaemonStatus.Online) + res += String.Format(visSecStr, SecurityWord()); + return res; + } + + /// + public ushort Port() + { + return Config.Port; + } + + /// + public bool ShutdownInProgress() + { + lock (watchdogLock) + { + return AwaitingShutdown != ShutdownRequestPhase.None; + } + } + + /// + public string WorldAnnounce(string message) + { + var res = SendCommand(SCWorldAnnounce + ";message=" + Program.SanitizeTopicString(message)); + if (res == "SUCCESS") + return null; + return res; + } + + /// + public bool Webclient() + { + return Config.Webclient; + } + + /// + public void SetWebclient(bool on) + { + lock (watchdogLock) { + var diff = on != Config.Webclient; + if (diff) + { + Config.Webclient = on; + RequestRestart(); + } + } + } + } +} diff --git a/TGServerService/Interop.cs b/TGServerService/ServerInstance/Interop.cs similarity index 78% rename from TGServerService/Interop.cs rename to TGServerService/ServerInstance/Interop.cs index 5c8040162c..62bd60b49c 100644 --- a/TGServerService/Interop.cs +++ b/TGServerService/ServerInstance/Interop.cs @@ -1,252 +1,251 @@ -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); - 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 + sealed partial class ServerInstance : ITGInterop + { + + object topicLock = new object(); + const int CommsKeyLen = 64; + string serviceCommsKey; //regenerated every DD restart + + //range of supported api versions + const int AllowedMajorAPIVersion = 1; + 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), MessageType.GameInfo); + break; + case SRKillProcess: + KillMe(); + break; + case SRIRCAdminChannelMessage: + SendMessage("RELAY: " + String.Join(" ", splits), MessageType.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 (3.{2}.x.x). Interop disabled.", splits.Count > 1 ? splits[1] : "NULL", AllowedMajorAPIVersion), 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() + { + //major will never change for all of TGS3 + //we treat minor as major, build as minor, and revision as patch + return !(GameAPIVersion == null || GameAPIVersion.Minor != AllowedMajorAPIVersion); + } + + //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/ServerInstance/PreactionHandler.cs b/TGServerService/ServerInstance/PreactionHandler.cs new file mode 100644 index 0000000000..7a4ef27723 --- /dev/null +++ b/TGServerService/ServerInstance/PreactionHandler.cs @@ -0,0 +1,102 @@ +using System; +using System.Diagnostics; +using System.IO; + +namespace TGServerService +{ + // Some useful functions for triggering pre action events + sealed partial class ServerInstance + { + /// + /// The instance directory for Preaction handlers + /// + const string EventFolder = "EventHandlers/"; + + /// + /// Creates the + /// + void InitEventHandlers() + { + Directory.CreateDirectory(EventFolder); + } + + /// + /// Gets the path of an event given an + /// + /// The name of the event + /// The path to the event handler + string GetEventPath(string eventName) + { + return string.Format("{0}{1}.bat", EventFolder, eventName); + } + + /// + /// Check if an event handler for exists + /// + /// The name of the event + /// if the event handler exists, otherwise + bool EventHandlerExists(string eventName) + { + return File.Exists(GetEventPath(eventName)); + } + + /// + /// Runs an event named if it exists + /// + /// The name of the event + /// if the event handler exists and failed to run, otherwise + bool HandleEvent(string eventName) + { + + if (!EventHandlerExists(eventName)) + { + // We don't need a handler, so let's just fail silently. + return true; + } + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = GetEventPath(eventName), + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + process.Start(); + process.WaitForExit(); + + var stdout = process.StandardOutput.ReadToEnd(); + var stderr = process.StandardError.ReadToEnd(); + var success = process.ExitCode == 0; + var eventData = String.Format("Preaction Event: {0} @ {1} ran. Stdout:\n{2}\nStderr:\n{3}", eventName, GetEventPath(eventName), stdout, stderr); + + if (success) + Service.WriteInfo(eventData, EventID.PreactionEvent); + else + Service.WriteWarning(eventData, EventID.PreactionFail); + + return success; + } + + /// + /// Run the "precompile" event + /// + /// if the event handler exists and failed to run, otherwise + public bool PrecompileHook() + { + return HandleEvent("precompile"); + } + + /// + /// Run the "postcompile" event + /// + /// if the event handler exists and failed to run, otherwise + public bool PostcompileHook() + { + return HandleEvent("postcompile"); + } + } +} diff --git a/TGServerService/Repository.cs b/TGServerService/ServerInstance/Repository.cs similarity index 61% rename from TGServerService/Repository.cs rename to TGServerService/ServerInstance/Repository.cs index 89cb815326..03efc8f041 100644 --- a/TGServerService/Repository.cs +++ b/TGServerService/ServerInstance/Repository.cs @@ -1,1259 +1,1324 @@ -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 PrivateKeyPath = "RepoKey/private_key.txt"; - const string PublicKeyPath = "RepoKey/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 ChangelogPathsToStage = 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 { } - try - { - ChangelogPathsToStage = LoadArray(details["synchronize_paths"]); - } - catch { } - } - catch { - ChangelogSupport = false; - } - 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(ChangelogPathsToStage, other.ChangelogPathsToStage) - && 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(ChangelogPathsToStage); - 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() - { - 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 PushChangelog() - { - 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.ChangelogPathsToStage) - 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.WriteError("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 +{ + sealed partial class ServerInstance : ITGRepository, IDisposable + { + /// + /// The directory for the repository + /// + const string RepoPath = "Repository"; + /// + /// The path to the Repository's json + /// + const string RepoTGS3SettingsPath = RepoPath + "/TGS3.json"; + /// + /// Path to the 's json + /// + const string CachedTGS3SettingsPath = "TGS3.json"; + /// + /// Error message for when a merge operation fails due to the target branch already having the source branch's commits + /// + const string RepoErrorUpToDate = "Already up to date!"; + /// + /// Git remote for push operations + /// + const string SSHPushRemote = "ssh_push_target"; + /// + /// The directory for the repository SSH keys + /// + const string RepoKeyDir = "RepoKey/"; + /// + /// The path to the private ssh-rsa key file + /// + const string PrivateKeyPath = RepoKeyDir + "private_key.txt"; + /// + /// The path to the public ssh-rsa key file + /// + const string PublicKeyPath = RepoKeyDir + "public_key.txt"; + /// + /// File name for monitoring which github pull requests are currently test merged + /// + const string PRJobFile = "prtestjob.json"; + /// + /// The branch that points to the current commit that is live or staged to be live in DreamDaemon + /// + const string LiveTrackingBranch = "___TGSLiveCommitTrackingBranch"; + /// + /// Commit message for + /// + const string CommitMessage = "Automatic changelog compile, [ci skip]"; + + /// + /// Used in conjunction with for multithreading safety + /// + object RepoLock = new object(); + /// + /// Used in conjunction with for multithreading safety + /// + bool RepoBusy = false; + /// + /// Whether or not a git clone operation is in progress + /// + bool Cloning = false; + + /// + /// The repository object + /// + Repository Repo; + /// + /// Used for reporting operation progress to the + /// + int currentProgress = -1; + + /// + /// Used for automatically updating the + /// + System.Timers.Timer autoUpdateTimer = new System.Timers.Timer() + { + AutoReset = true + }; + + /// + /// Initializes the repository + /// + void InitRepo() + { + Directory.CreateDirectory(RepoKeyDir); + if(Exists()) + UpdateInterfaceDll(false); + if(LoadRepo() == null) + DisableGarbageCollectionNoLock(); + //start the autoupdate timer + autoUpdateTimer.Elapsed += AutoUpdateTimer_Elapsed; + SetAutoUpdateInterval(Config.AutoUpdateInterval); + } + + /// + /// Checks if the and json files match. + /// + /// if the jsons match, otherwise + bool RepoConfigsMatch() + { + //this should never be called while the repo is busy + RepoConfig I = null; + lock (RepoLock) + { + if (!RepoBusy && LoadRepo() == null) + I = new RepoConfig(RepoTGS3SettingsPath); + } + if (I == null) + throw new Exception("Unable to load TGS3.json from repo!"); + var J = GetCachedRepoConfig(); + return I == J; + } + + /// + /// Gets the for + /// + /// The for + RepoConfig GetCachedRepoConfig() + { + return new RepoConfig(CachedTGS3SettingsPath); + } + + /// + public bool OperationInProgress() + { + lock (RepoLock) + { + return RepoBusy; + } + } + + /// + public int CheckoutProgress() + { + return currentProgress; + } + + /// + /// Initializes + /// + /// on success, error message on failure + 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 + /// + void DisposeRepo() + { + if (Repo != null) + { + Repo.Dispose(); + Repo = null; + } + } + + /// + public bool Exists() + { + lock (RepoLock) + { + return !Cloning && Repository.IsValid(RepoPath); + } + } + + /// + /// Updates with the progess of the current transfer operation + /// + /// The of the current transfer operation + /// + bool HandleTransferProgress(TransferProgress progress) + { + currentProgress = ((int)(((float)progress.ReceivedObjects / progress.TotalObjects) * 100) / 2) +( (int)(((float)progress.IndexedObjects / progress.TotalObjects) * 100) / 2); + return true; + } + + /// + /// Updates with the progess of the current checkout operation + /// + /// Ignored + /// Dividend for progress calculation + /// Divisor for progress calculation + void HandleCheckoutProgress(string path, int completedSteps, int totalSteps) + { + currentProgress = (int)(((float)completedSteps / totalSteps) * 100); + } + + /// + /// Backups up the and deletes the current if they exist. Clones the given branch of the given remote. Sets up new + /// + /// Remote and branch name, seperated by a ' ' + void Clone(object twostrings) + { + //busy flag set by caller + var ts = ((string)twostrings).Split(' '); + var RepoURL = ts[0]; + var BranchName = ts[1]; + try + { + SendMessage(String.Format("REPO: {2} started: Cloning {0} branch of {1} ...", BranchName, RepoURL, Repository.IsValid(RepoPath) ? "Full reset" : "Setup"), MessageType.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!", MessageType.DeveloperInfo); + Service.WriteInfo("Repository {0}:{1} successfully cloned", EventID.RepoClone); + } + finally + { + currentProgress = -1; + } + } + catch (Exception e) + + { + SendMessage("REPO: Setup failed!", MessageType.DeveloperInfo); + Service.WriteWarning(String.Format("Failed to clone {2}:{0}: {1}", BranchName, e.ToString(), RepoURL), EventID.RepoCloneFail); + } + finally + { + lock (RepoLock) + { + RepoBusy = false; + Cloning = false; + } + } + } + + /// + /// Turns off the gc.auto git config setting + /// + + void DisableGarbageCollectionNoLock() + { + Repo.Config.Set("gc.auto", false); + } + + /// + /// Copies the Static directory to the first available Static_BACKUP path in the then deleted the old directory + /// + 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); + } + + /// + /// Updates the with the + /// + /// on success, error message on failure + 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; + } + + /// + /// Initial setup for the and + /// + void InitialConfigureRepository() + { + Directory.CreateDirectory(StaticDirs); + UpdateInterfaceDll(false); + UpdateTGS3Json(); + var Config = GetCachedRepoConfig(); //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.WriteError("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.WriteError("Could not setup static DLL: " + I, EventID.RepoConfigurationFail); + } + } + } + + /// + 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(RepoURL + ' ' + BranchName); + return null; + } + } + + /// + /// Gets the SHA or branch name of the 's HEAD + /// + /// on success, error message on failure + /// If , returns the branch name instead of the SHA + /// If , returns the tracked branch SHA and ignores + /// The SHA or branch name of the 's HEAD + 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; + } + } + } + + /// + public string GetHead(bool useTracked, out string error) + { + return GetShaOrBranch(out error, false, useTracked); + } + + /// + public string GetBranch(out string error) + { + return GetShaOrBranch(out error, true, false); + } + + /// + 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; + } + } + + /// + /// Equivalent of running `git reset --hard` on the repository. Requires + /// + /// If not , reset to this branch instead of HEAD + /// on success, error message on failure + 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 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, MessageType.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!", MessageType.DeveloperInfo); + Service.WriteInfo("Repo checked out " + sha, EventID.RepoCheckout); + return res; + } + catch (Exception e) + { + SendMessage("REPO: Checkout failed!", MessageType.DeveloperInfo); + Service.WriteWarning(String.Format("Repo checkout of {0} failed: {1}", sha, e.ToString()), EventID.RepoCheckoutFail); + return e.ToString(); + } + } + } + + /// + /// Merges given into the current branch + /// + /// The sha/branch/tag to merge + /// on success, error message on failure + string MergeBranch(string committish) + { + var mo = new MergeOptions() + { + OnCheckoutProgress = HandleCheckoutProgress + }; + var Result = Repo.Merge(committish, MakeSig()); + currentProgress = -1; + switch (Result.Status) + { + case MergeStatus.Conflicts: + ResetNoLock(null); + SendMessage("REPO: Merge conflicted, aborted.", MessageType.DeveloperInfo); + return "Merge conflict occurred."; + case MergeStatus.UpToDate: + return RepoErrorUpToDate; + } + return null; + } + + /// + public string Update(bool reset) + { + return UpdateImpl(reset, true); + } + + /// + /// Fetches the origin and merges it into the current branch + /// + /// If , the operation will perform a hard reset instead of a merge + /// If , a return value of will be changed to + /// on success, error message on failure + 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"), MessageType.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!", MessageType.DeveloperInfo); + Service.WriteWarning(String.Format("Repo{0} update failed", reset ? " hard" : ""), reset ? EventID.RepoHardUpdateFail : EventID.RepoMergeUpdateFail); + return E.ToString(); + } + } + } + + /// + /// Properly updates any git submodules in the repository + /// + 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), MessageType.DeveloperInfo); + Service.WriteWarning(msg, EventID.SubmoduleReclone); + } + } + + /// + /// Creates a date and timestamped tag of the current HEAD + /// + /// on success, error message on failure + 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(); + } + } + + /// + /// Lists tags in the repository created by the + /// + /// on success, error message on failure + /// A dictionary of tag title -> commit SHA on success, on failure + 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 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 " : ""), MessageType.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; + } + } + + /// + /// Creates a commit based off of the configured name and e-mail + /// + /// The created + Signature MakeSig() + { + return new Signature(new Identity(Config.CommitterName, Config.CommitterEmail), DateTimeOffset.Now); + } + + /// + /// Deletes the 's + /// + void DeletePRList() + { + if (File.Exists(PRJobFile)) + try + { + File.Delete(PRJobFile); + } + catch (Exception e) + { + Service.WriteError("Failed to delete PR list: " + e.ToString(), EventID.RepoPRListError); + } + } + + /// + /// Deserializes the + /// + /// A of . The outer one is keyed by PR# the inner one is keyed by internal s + IDictionary> GetCurrentPRList() + { + if (!File.Exists(PRJobFile)) + return new Dictionary>(); + var rawdata = File.ReadAllText(PRJobFile); + var Deserializer = new JavaScriptSerializer(); + return Deserializer.Deserialize>>(rawdata); + } + + /// + /// Serializes pull request info into + /// + /// + void SetCurrentPRList(IDictionary> list) + { + var Serializer = new JavaScriptSerializer(); + var rawdata = Serializer.Serialize(list); + File.WriteAllText(PRJobFile, rawdata); + } + + /// + public string MergePullRequest(int PRNumber) + { + lock (RepoLock) + { + var result = LoadRepo(); + if (result != null) + return result; + SendMessage(String.Format("REPO: Merging PR #{0}...", PRNumber), MessageType.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?", MessageType.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!", MessageType.DeveloperInfo); + Service.WriteWarning(String.Format("Failed to merge pull request #{0}: {1}", PRNumber, E.ToString()), EventID.RepoPRMergeFail); + return E.ToString(); + } + } + } + + /// + 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 string GetCommitterName() + { + lock (RepoLock) + { + return Config.CommitterName; + } + } + + /// + public void SetCommitterName(string newName) + { + lock (RepoLock) + { + Config.CommitterName = newName; + } + } + + /// + public string GetCommitterEmail() + { + lock (RepoLock) + { + return Config.CommitterEmail; + } + } + + /// + public void SetCommitterEmail(string newEmail) + { + lock (RepoLock) + { + Config.CommitterEmail = newEmail; + } + } + + /// + public string SynchronizePush() + { + var Config = GetCachedRepoConfig(); + 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"; + } + + /// + /// Create that Prune and have the appropriate credentials and progress handler + /// + /// Properly configured + 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(); + } + } + + /// + /// Check if the current HEAD matches the tracked remote branch HEAD + /// + /// if the current HEAD matches the tracked remote branch HEAD, otherwise + 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; + } + } + } + + /// + /// Create a commit based on a + /// + /// A with + /// on success, error message on failure + 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.WriteWarning("Repo commit failed: " + e.ToString(), EventID.RepoCommitFail); + return e.ToString(); + } + } + } + + /// + 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.WriteWarning("Repo push failed: " + e.ToString(), EventID.RepoPushFail); + return e.ToString(); + } + } + } + + /// + /// Check if the is configured for SSH pushing + /// + /// if the see cref="ServerInstance"/> is configured for SSH pushing, otherwise + bool SSHAuth() + { + return File.Exists(PrivateKeyPath) && File.Exists(PublicKeyPath); + } + + /// + /// SSH credentials callback. Properly sets up SSH keys for an authorization operation + /// + /// Ignored + /// The username to use for the operation + /// The for the operation + /// The proper for the operation + 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 string GenerateChangelog(out string error) + { + return GenerateChangelogImpl(out error); + } + + /// + /// Updates the html changelog + /// + /// on success, error on failure + /// If , prevents a recursive call to this function after updating pip dependencies + /// The output of the python script + public string GenerateChangelogImpl(out string error, bool recurse = false) + { + var RConfig = GetCachedRepoConfig(); + 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(); + } + } + Config.AutoUpdateInterval = newInterval; + } + + /// + public ulong AutoUpdateInterval() + { + return Config.AutoUpdateInterval; + } + + /// + /// Runs on the configured and tries to and the + /// + /// A + /// The event arguments + private void AutoUpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + { + if (UpdateImpl(true, false) == null) + { + Compile(true); + } + } + + /// + 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; + } + + /// + /// Updates with the staged 's SHA + /// + /// The commit SHA that was staged + void UpdateLiveSha(string newSha) + { + if (LoadRepo() != null) + return; + var B = Repo.Branches[LiveTrackingBranch]; + if (B != null) + Repo.Branches.Remove(B); + Repo.CreateBranch(LiveTrackingBranch, newSha); + } + + /// + /// Gets the current staged or live commit SHA + /// + /// The current staged or live commit SHA + public string LiveSha() + { + var B = Repo.Branches[LiveTrackingBranch]; + return B != null ? B.Tip.Sha : "UNKNOWN"; + } + } +} diff --git a/TGServerService/Instance.cs b/TGServerService/ServerInstance/ServerInstance.cs similarity index 55% rename from TGServerService/Instance.cs rename to TGServerService/ServerInstance/ServerInstance.cs index f3d9a813e7..50a3cd7c80 100644 --- a/TGServerService/Instance.cs +++ b/TGServerService/ServerInstance/ServerInstance.cs @@ -1,24 +1,32 @@ using System; using System.ServiceModel; -using TGServiceInterface; +using TGServiceInterface.Components; namespace TGServerService { //I know the fact that this is one massive partial class is gonna trigger everyone - //There really was no other succinct way to do it + //There really was no other succinct way to do it (<= He's lying through his teeth, don't listen to him) - //this line basically says make one instance of the service, use it multithreaded for requests, and never delete it + //this line basically says take one instance of the service, use it multithreaded for requests, and never delete it + + /// + /// The class which holds all interface components. There are no safeguards for call race conditions so these must be guarded against internally + /// [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.Single)] - partial class TGStationServer : IDisposable, ITGConnectivity + sealed partial class ServerInstance : IDisposable, ITGConnectivity { + /// + /// The configuration settings for the instance + /// public readonly InstanceConfig Config; - - //call partial constructors/destructors from here - //called when the service is started - public TGStationServer(InstanceConfig config) + /// + /// Constructs and a + /// + public ServerInstance(InstanceConfig config) { Config = config; FindTheDroidsWereLookingFor(); + InitEventHandlers(); InitChat(); InitRepo(); InitByond(); @@ -26,7 +34,9 @@ namespace TGServerService InitDreamDaemon(); } - //called when the service is stopped + /// + /// Cleans up the + /// void RunDisposals() { DisposeDreamDaemon(); @@ -34,31 +44,38 @@ namespace TGServerService DisposeByond(); DisposeRepo(); DisposeChat(); - Config.Save(); } - //public api + /// public string Version() { - return TGServerService.VersionString; + return Service.VersionString; } /// public void VerifyConnection() { } + /// public void Reattach(bool silent) { Config.ReattachRequired = true; if(!silent) - SendMessage("SERVICE: Update started...", ChatMessageType.DeveloperInfo); + SendMessage("SERVICE: Update started...", MessageType.DeveloperInfo); } //mostly generated code with a call to RunDisposals() //you don't need to open this #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls + /// + /// To detect redundant calls + /// + private bool disposedValue = false; - protected virtual void Dispose(bool disposing) + /// + /// Implements the pattern. Calls + /// + /// if was called manually, if it was from the finalizer + void Dispose(bool disposing) { if (!disposedValue) { @@ -82,6 +99,9 @@ namespace TGServerService // } // This code added to correctly implement the disposable pattern. + /// + /// Implements the pattern + /// public void Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. diff --git a/TGServerService/Service.Designer.cs b/TGServerService/Service.Designer.cs deleted file mode 100644 index 029f1291c9..0000000000 --- a/TGServerService/Service.Designer.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace TGServerService -{ - partial class TGServerService - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Component Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - // - // Service1 - // - this.ServiceName = "TG Station Server"; - - } - - #endregion - } -} diff --git a/TGServerService/Service.cs b/TGServerService/Service.cs index d34644f570..8cd43a8c54 100644 --- a/TGServerService/Service.cs +++ b/TGServerService/Service.cs @@ -6,130 +6,93 @@ using System.Security.Principal; using System.ServiceModel; using System.ServiceProcess; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGServerService { - public partial class TGServerService : ServiceBase, ITGConnectivity, ITGSService - { - //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, - InstanceInitializationFailure = 7600, - } - - static TGServerService ActiveService; //So everyone else can write to our eventlog - + /// + /// The windows service the application runs as + /// + sealed partial class Service : ServiceBase + { + /// + /// The service version based on the + /// public static readonly string VersionString = "/tg/station 13 Server Service v" + FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).FileVersion; /// - /// You can't write to logs while impersonating, call this to cancel WCF's impersonation first + /// Singleton instance + /// + static Service ActiveService; + + /// + /// Cancels WCF's user impersonation to allow clean access to writing log files /// public static void CancelImpersonation() { WindowsIdentity.Impersonate(IntPtr.Zero); } + /// + /// Writes information to Windows the event log + /// + /// The log message + /// The of the message public static void WriteInfo(string message, EventID id) { ActiveService.EventLog.WriteEntry(message, EventLogEntryType.Information, (int)id); } + /// + /// Writes an error to the Windows event log + /// + /// The log message + /// The of the message public static void WriteError(string message, EventID id) { ActiveService.EventLog.WriteEntry(message, EventLogEntryType.Error, (int)id); } + /// + /// Writes a warning to the Windows event log + /// + /// The log message + /// The of the message public static void WriteWarning(string message, EventID id) { ActiveService.EventLog.WriteEntry(message, EventLogEntryType.Warning, (int)id); } + /// + /// Writes an access event to the Windows event log + /// + /// The (un)authenticated Windows user's name + /// if authenticated sucessfully, otherwise public static void WriteAccess(string username, bool authSuccess) { ActiveService.EventLog.WriteEntry(String.Format("Access from: {0}", username), authSuccess ? EventLogEntryType.SuccessAudit : EventLogEntryType.FailureAudit, (int)EventID.Authentication); } - + + /// + /// The WCF host that contains connects to + /// ServiceHost serviceHost; IDictionary hosts; + /// + /// Migrates the .NET config from to + /// + /// The version to migrate from + /// The version to migrate to void MigrateSettings(int oldVersion, int newVersion) { - //TODO + //Uneeded... So far... } //you should seriously not add anything here //Use OnStart instead - public TGServerService() + /// + /// Construct and run a . Can only execute in the context of the Windows service manager + /// + public Service() { var Config = Properties.Settings.Default; try @@ -148,7 +111,7 @@ namespace TGServerService Config.Save(); } ActiveService = this; - InitializeComponent(); + ServiceName = "TG Station Server"; Run(this); } finally @@ -194,7 +157,7 @@ namespace TGServerService void SetupService() { serviceHost = CreateHost(this); - AddEndpoint(serviceHost, typeof(ITGSService), Server.MasterInterfaceName); + AddEndpoint(serviceHost, typeof(ITGSService), Interface.MasterInterfaceName); serviceHost.Authorization.ServiceAuthorizationManager = new AdministrativeAuthorizationManager(); //only admins can diddle us } @@ -222,19 +185,19 @@ namespace TGServerService } ServiceHost SetupInstance(string path) { - TGStationServer instance; + ServerInstance instance; try { var config = InstanceConfig.Load(path); if (hosts.ContainsKey(path)) { - var datInstance = ((TGStationServer)hosts[path].SingletonInstance); + var datInstance = ((ServerInstance)hosts[path].SingletonInstance); WriteError(String.Format("Unable to start instance at path {0}. Has the same name as instance at path {1} ({2}). Detaching...", path, datInstance.ServerDirectory(), datInstance.Config.Name), EventID.InstanceInitializationFailure); return null; } if (!config.Enabled) return null; - instance = new TGStationServer(config); + instance = new ServerInstance(config); } catch (Exception e) { @@ -245,23 +208,28 @@ namespace TGServerService var host = CreateHost(instance); hosts.Add(instance.Config.Name, host); - var endpointPrefix = String.Format("{0}/{1}", Server.MasterInterfaceName, instance.Config.Name); - foreach (var J in Server.InstanceInterfaces) + var endpointPrefix = String.Format("{0}/{1}", Interface.MasterInterfaceName, instance.Config.Name); + foreach (var J in Interface.ValidInterfaces) AddEndpoint(host, J, endpointPrefix); host.Authorization.ServiceAuthorizationManager = instance; return host; } - //shorthand for adding the WCF endpoint + /// + /// Adds a WCF endpoint for a component + /// + /// + /// + /// void AddEndpoint(ServiceHost host, Type typetype, string PipePrefix) { var bindingName = PipePrefix + "/" + typetype.Name; - host.AddServiceEndpoint(typetype, new NetNamedPipeBinding() { SendTimeout = new TimeSpan(0, 0, 30), MaxReceivedMessageSize = Server.TransferLimitLocal }, bindingName); + host.AddServiceEndpoint(typetype, new NetNamedPipeBinding() { SendTimeout = new TimeSpan(0, 0, 30), MaxReceivedMessageSize = Interface.TransferLimitLocal }, bindingName); var httpsBinding = new WSHttpBinding() { SendTimeout = new TimeSpan(0, 0, 40), - MaxReceivedMessageSize = Server.TransferLimitRemote + MaxReceivedMessageSize = Interface.TransferLimitRemote }; var requireAuth = typetype.Name != typeof(ITGConnectivity).Name; httpsBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None; @@ -270,7 +238,9 @@ namespace TGServerService host.AddServiceEndpoint(typetype, httpsBinding, bindingName); } - //when we is kill + /// + /// Shutsdown the WCF and calls on it's + /// protected override void OnStop() { try @@ -278,7 +248,7 @@ namespace TGServerService foreach (var I in hosts) { var host = I.Value; - TGStationServer instance = (TGStationServer)host.SingletonInstance; + var instance = (ServerInstance)host.SingletonInstance; host.Close(); instance.Dispose(); } @@ -296,7 +266,7 @@ namespace TGServerService public void PrepareForUpdate() { foreach (var I in hosts) - ((TGStationServer)I.Value.SingletonInstance).Reattach(false); + ((ServerInstance)I.Value.SingletonInstance).Reattach(false); } /// diff --git a/TGServerService/Service.resx b/TGServerService/Service.resx deleted file mode 100644 index e5858cc294..0000000000 --- a/TGServerService/Service.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - False - - \ No newline at end of file diff --git a/TGServerService/TGServerService.csproj b/TGServerService/TGServerService.csproj index 39f43c678b..759d3fcac8 100644 --- a/TGServerService/TGServerService.csproj +++ b/TGServerService/TGServerService.csproj @@ -16,29 +16,31 @@ - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - true - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - tgs.ico + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + true + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + true + true + bin\x86\Release\TGServerService.xml + ..\packages\Discord.Net.Core.1.0.2\lib\net45\Discord.Net.Core.dll @@ -80,28 +82,32 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + True True Settings.settings - + Component @@ -111,9 +117,6 @@ Component - - Service.cs - @@ -134,9 +137,6 @@ ProjectInstaller.cs - - Service.cs - diff --git a/TGServiceInstaller/Product.wxs b/TGServiceInstaller/Product.wxs index ed5c40da2e..a43f25374b 100644 --- a/TGServiceInstaller/Product.wxs +++ b/TGServiceInstaller/Product.wxs @@ -47,11 +47,11 @@ INSTALLSHORTCUTSTART = 1 INSTALLSHORTCUTDESK = 1 diff --git a/TGServiceInstaller/TGServiceInstaller.wixproj b/TGServiceInstaller/TGServiceInstaller.wixproj index d57b13bb7d..fd47e7c7f5 100644 --- a/TGServiceInstaller/TGServiceInstaller.wixproj +++ b/TGServiceInstaller/TGServiceInstaller.wixproj @@ -18,6 +18,7 @@ bin\$(Configuration)\ obj\$(Configuration)\ True + True diff --git a/TGServiceInterface/Byond.cs b/TGServiceInterface/Byond.cs deleted file mode 100644 index 4da1105271..0000000000 --- a/TGServiceInterface/Byond.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System.ServiceModel; - -namespace TGServiceInterface -{ - /// - /// 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 - /// - [ServiceContract] - public interface ITGByond - { - /// - /// Gets the current status of any BYOND updates - /// - /// A TGByondStatus - [OperationContract] - TGByondStatus CurrentStatus(); - - /// - /// updates the used byond version to that of version major.minor - /// The change won't take place until dream daemon reboots - /// the latest parameter overrides the other two and forces an update to the latest (beta?) version - /// runs asyncronously, use CurrentStatus to see progress - /// - /// Major BYOND version. E.g. 511 - /// Minor BYOND version. E.g. 1381 - /// True if the update started, false if another operation was in progress - [OperationContract] - bool UpdateToVersion(int major, int minor); - - /// - /// Check the last update error - /// Checking this will clear the value - /// - /// The last update error, if any - [OperationContract] - string GetError(); - - /// - /// Get the currently installed version as a string formatted as Major.Minor - /// - /// The type of version to retrieve - /// null if no version is detected, the version string otherwise - [OperationContract] - string GetVersion(TGByondVersion type); - } -} diff --git a/TGServiceInterface/Chat.cs b/TGServiceInterface/ChatSetupInfo.cs similarity index 69% rename from TGServiceInterface/Chat.cs rename to TGServiceInterface/ChatSetupInfo.cs index cfdab6d228..ddbe90156d 100644 --- a/TGServiceInterface/Chat.cs +++ b/TGServiceInterface/ChatSetupInfo.cs @@ -1,56 +1,17 @@ using System; using System.Collections.Generic; using System.Runtime.Serialization; -using System.ServiceModel; 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; @@ -60,14 +21,27 @@ namespace TGServiceInterface const int GameChannelIndex = 5; const int ProviderIndex = 6; const int EnabledIndex = 7; - protected const int BaseIndex = 8; - protected readonly bool InitializeFields; /// - /// Constructs a TGChatSetupInfo from optional past data + /// Starting index of which child classes should use to write their custom data to + /// + protected const int BaseIndex = 8; + /// + /// Set to if a child constructor should use the baseInfo parameter of to initialize it's property fields, otherwise + /// + protected readonly bool InitializeFields; + + /// + /// Raw access to the underlying data + /// + [DataMember] + public IList DataFields { get; protected set; } + + /// + /// 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; @@ -90,43 +64,55 @@ namespace TGServiceInterface DataFields = baseInfo.DataFields; } - TGChatSetupInfo Specialize() + /// + /// Recreates as the correct child + /// + /// 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!"); } } - //trims and adds the leading # - protected virtual string SanitizeChannelName(string working) + /// + /// Properly formats a name for the + /// + /// The to format + /// The formatted + protected virtual string SanitizeChannelName(string channel) { - return Specialize().SanitizeChannelName(working); - } - void SanitizeChannelNames(IList working) - { - for (var I = 0; I < working.Count; ++I) - - if (String.IsNullOrWhiteSpace(working[I])) - { - working.RemoveAt(I); - --I; - } - else - working[I] = SanitizeChannelName(working[I].Trim()); + return Specialize().SanitizeChannelName(channel); } /// - /// Constructs a TGChatSetupInfo from a data list + /// Sanitizes a list of + /// + /// An of strings + void SanitizeChannelNames(IList channelnames) + { + for (var I = 0; I < channelnames.Count; ++I) + + if (String.IsNullOrWhiteSpace(channelnames[I])) + { + channelnames.RemoveAt(I); + --I; + } + else + channelnames[I] = SanitizeChannelName(channelnames[I].Trim()); + } + + /// + /// Constructs a from a data list /// /// The data - /// The chat provider - public TGChatSetupInfo(IList DeserializedData) + public ChatSetupInfo(IList DeserializedData) { DataFields = DeserializedData; } @@ -206,25 +192,18 @@ 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); } } - - /// - /// Raw access to the underlying data - /// - [DataMember] - public IList DataFields { get; protected set; } } /// - /// Chat provider for IRC - /// Admin entries should be user nicknames in normal mode or required flags in special mode + /// Chat provider for IRC. Admin entries should be user nicknames in normal mode or required channel flags in special mode /// [DataContract] - public class TGIRCSetupInfo : TGChatSetupInfo + public sealed class IRCSetupInfo : ChatSetupInfo { const int URLIndex = 0; const int PortIndex = 1; @@ -238,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"; @@ -252,7 +231,8 @@ namespace TGServiceInterface AuthLevel = IRCMode.Op; } } - + + /// protected override string SanitizeChannelName(string working) { if (working[0] != '#') @@ -263,9 +243,10 @@ namespace TGServiceInterface /// /// The port of the IRC server /// - public ushort Port { + public ushort Port + { get { return Convert.ToUInt16(DataFields[BaseIndex + PortIndex]); } - set { DataFields[BaseIndex + PortIndex] = value.ToString(); } + set { DataFields[BaseIndex + PortIndex] = value.ToString(); } } /// /// The URL of the IRC server @@ -310,11 +291,10 @@ namespace TGServiceInterface } /// - /// Chat provider for Discord - /// Admin entires should be user ids in normal mode or group ids in special mode + /// Chat provider for Discord. Admin entires should be user ids in normal mode or group ids in special mode /// [DataContract] - public class TGDiscordSetupInfo : TGChatSetupInfo + public sealed class DiscordSetupInfo : ChatSetupInfo { const int BotTokenIndex = 0; const int FieldsLen = 1; @@ -322,15 +302,16 @@ 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 } + /// protected override string SanitizeChannelName(string working) { - working = working.Replace("<", "").Replace(">", "").Replace("&", ""); //filter out some stuff that can come in the copypasta + working = working.Replace("<", "").Replace(">", "").Replace("&", ""); //filter out some stuff that can come in the copypasta try { Convert.ToUInt64(working); @@ -351,41 +332,4 @@ namespace TGServiceInterface set { DataFields[BaseIndex + BotTokenIndex] = value; } } } - - /// - /// Interface for handling chat bot - /// - [ServiceContract] - public interface ITGChat - { - /// - /// Set the chat provider info - /// - /// The info to set - [OperationContract] - string SetProviderInfo(TGChatSetupInfo info); - /// - /// Returns the chat provider info - /// - /// The type of provider to get info for - /// The chat provider info for the selected provider - [OperationContract] - IList ProviderInfos(); - - /// - /// Checks connection status - /// - /// The type of provider to check if connected - /// true if connected, false otherwise - [OperationContract] - bool Connected(TGChatProvider providerType); - - /// - /// Reconnect to the chat service - /// - /// The type of provider to reconnect - /// null on success, error message on failure - [OperationContract] - string Reconnect(TGChatProvider providerType); - } } diff --git a/TGServiceInterface/Command.cs b/TGServiceInterface/Command.cs index ebdcefac50..c018a9c6b1 100644 --- a/TGServiceInterface/Command.cs +++ b/TGServiceInterface/Command.cs @@ -4,118 +4,89 @@ using System.Threading; namespace TGServiceInterface { - public enum ExitCode - { - Normal = 0, - ConnectionError = 1, - BadCommand = 2, - 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 + /// public static ThreadLocal> OutputProcVar = new ThreadLocal>(); + /// + /// Write output to the invoker + /// + /// The output to display protected static void OutputProc(string message) { OutputProcVar.Value(message); } + /// + /// The text that invokes this . Set in constructor + /// public string Keyword { get; protected set; } - public Command[] Children { get; protected set; } = { }; + /// + /// The number of parameters this requires. Set in Constructor + /// public int RequiredParameters { get; protected set; } + /// + /// Caller of , can be used to modify the root behaviour of the + /// + /// List of parameters passed to the + /// An describing the execution of the public virtual ExitCode DoRun(IList parameters) { return Run(parameters); } + /// + /// Override to do the actions of the + /// + /// List of parameters passed to the . Guaranteed to have at least non-empty/whitespace entries + /// An describing the execution of the protected abstract ExitCode Run(IList parameters); + /// + /// Prints usage text of the to the invoker + /// public virtual void PrintHelp() { var argstr = GetArgumentString(); OutputProc(String.Format("{0} {1}- {2}", Keyword, argstr.Length > 0 ? argstr + " " : "", GetHelpText())); } + /// + /// Override to add argument text to the + /// Format is <required> <arguments> [optional] [arguments] + /// + /// Formatted argument text for the public virtual string GetArgumentString() { return ""; } + /// + /// Override to add usage text to the + /// + /// Formatted usage text for the public abstract string GetHelpText(); } - - public class RootCommand : Command - { - public static bool PrintHelpList = false; - protected override ExitCode Run(IList parameters) - { - if (parameters.Count > 0) - { - var LocalKeyword = parameters[0].Trim().ToLower(); - parameters.RemoveAt(0); - switch (LocalKeyword) - { - case "help": - case "?": - PrintHelp(); - return ExitCode.Normal; - default: - foreach (var c in Children) - if (c.Keyword == LocalKeyword) - { - if(parameters.Count> 0) - { - var possibleHelp = parameters[0].ToLower(); - if (possibleHelp == "help" || possibleHelp == "?") - { - c.PrintHelp(); - return ExitCode.Normal; - } - } - if (parameters.Count < c.RequiredParameters) - { - OutputProc("Not enough parameters!"); - return ExitCode.BadCommand; - } - return c.DoRun(parameters); - } - parameters.Insert(0, LocalKeyword); - break; - } - } - OutputProc(String.Format("Invalid command! Type '{0}?' or '{0}help' for available commands.", Keyword != null ? Keyword + " " : "")); - return ExitCode.BadCommand; - } - public override void PrintHelp() - { - var Final = new List(); - if (PrintHelpList) - { - foreach (var c in Children) - Final.Add(c.Keyword); - OutputProc("Available commands (type '?' or 'help' after command for more info): " + String.Join(", ", Final)); - } - else - { - var Prefixes = new List(); - var Postfixes = new List(); - int MaxPrefixLen = 0; - foreach (var c in Children) - { - var ns = c.Keyword + " " + c.GetArgumentString(); - MaxPrefixLen = Math.Max(MaxPrefixLen, ns.Length); - Prefixes.Add(ns); - Postfixes.Add(c.GetHelpText()); - } - - for (var I = 0; I < Prefixes.Count; ++I) - { - var lp = Prefixes[I]; - for (; lp.Length < MaxPrefixLen + 1; lp += " ") ; - Final.Add(lp + "- " + Postfixes[I]); - } - Final.Sort(); - Final.ForEach(OutputProc); - } - } - - public override string GetHelpText() - { - throw new NotImplementedException(); - } - } } diff --git a/TGServiceInterface/Compiler.cs b/TGServiceInterface/Compiler.cs deleted file mode 100644 index 75684245fb..0000000000 --- a/TGServiceInterface/Compiler.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.ServiceModel; - -namespace TGServiceInterface -{ - /// - /// 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 - /// - [ServiceContract] - public interface ITGCompiler - { - /// - /// Sets up the symlinks for hotswapping game code - /// this will reset everything (except the static directories) - /// this will not reset the static directories - /// requires the repository to be set up and locks it for the duration of the operation - /// does not compile the game - /// runs asyncronously - /// - /// true if the operation began, false if it could not start - [OperationContract] - 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 compiler to be initialized - /// runs asyncronously - /// - /// If true no message for compilation start will be printed - /// true if the operation began, false if it could not start - [OperationContract] - bool Compile(bool silent = false); - - /// - /// Cancels the current compilation - /// - /// null on success, error message on failure - [OperationContract] - string Cancel(); - - /// - /// Returns the current compiler status - /// - /// The current compiler status - [OperationContract] - TGCompilerStatus GetStatus(); - - /// - /// Returns the error message of the last operation - /// Reading this will clear the stored value - /// - /// the error message of the last operation if it failed or null if it succeeded - [OperationContract] - string CompileError(); - - /// - /// Returns the relative path of the dme the compiler will look for without the .dme part - /// - /// The relative path of the dme the compiler will look for without the .dme part - [OperationContract] - string ProjectName(); - - /// - /// Sets the relative path of the dme the compiler will look for without the .dme part - /// - /// The relative path of the dme the compiler will look for without the .dme part - [OperationContract] - void SetProjectName(string projectName); - } -} diff --git a/TGServiceInterface/Administration.cs b/TGServiceInterface/Components/Administration.cs similarity index 76% rename from TGServiceInterface/Administration.cs rename to TGServiceInterface/Components/Administration.cs index c973f6f2ea..b177312820 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 @@ -11,7 +11,7 @@ namespace TGServiceInterface /// /// Returns the name of the windows group allowed to use the service other than administrator /// - /// The name of the windows group allowed to use the service other than administrator, "ADMIN" if it's unset, null on failure + /// The name of the windows group allowed to use the service other than administrator, "ADMIN" if it's unset, on failure [OperationContract] string GetCurrentAuthorizedGroup(); @@ -19,14 +19,14 @@ namespace TGServiceInterface /// Searches the windows machine for the group named , sets it as the authorized group if it's found /// /// The name of the windows group to search for or null to clear the setting - /// The name of the windows group that is now authorized to use the service on success, null on failure, "ADMIN" on clearing + /// The name of the windows group that is now authorized to use the service on success, on failure, "ADMIN" on clearing [OperationContract] string SetAuthorizedGroup(string groupName); /// /// Renames the current static folder to a backup name and recreates is from the current repo using TGS3.json /// - /// null on success, error message on failure + /// on success, error message on failure [OperationContract] string RecreateStaticFolder(); } diff --git a/TGServiceInterface/Components/Byond.cs b/TGServiceInterface/Components/Byond.cs new file mode 100644 index 0000000000..d640179d6c --- /dev/null +++ b/TGServiceInterface/Components/Byond.cs @@ -0,0 +1,43 @@ +using System.ServiceModel; + +namespace TGServiceInterface.Components +{ + + /// + /// For managing the BYOND installation the server runs + /// + [ServiceContract] + public interface ITGByond + { + /// + /// Gets the current status of any BYOND updates + /// + /// The current status of the byond updater + [OperationContract] + 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 + /// + /// Major BYOND version. E.g. 511 + /// Minor BYOND version. E.g. 1381 + /// if the update started, if another operation was in progress or DreamDaemon is running + [OperationContract] + bool UpdateToVersion(int major, int minor); + + /// + /// Check the last update error. Checking this will clear the value + /// + /// The last update error, if any. otherwise. + [OperationContract] + string GetError(); + + /// + /// Get the currently installed version as a string formatted as Major.Minor + /// + /// The type of version to retrieve + /// if no version is detected, the version string otherwise + [OperationContract] + string GetVersion(ByondVersion type); + } +} diff --git a/TGServiceInterface/Components/Chat.cs b/TGServiceInterface/Components/Chat.cs new file mode 100644 index 0000000000..e52659d3f7 --- /dev/null +++ b/TGServiceInterface/Components/Chat.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.ServiceModel; + +namespace TGServiceInterface.Components +{ + /// + /// Interface for handling chat bot + /// + [ServiceContract] + public interface ITGChat + { + /// + /// Sets a chat provider + /// + /// The info to set + [OperationContract] + string SetProviderInfo(ChatSetupInfo info); + /// + /// Returns for all s + /// + /// A list of all s + [OperationContract] + IList ProviderInfos(); + + /// + /// Checks connection status + /// + /// The type of provider to check if connected + /// if connected, otherwise + [OperationContract] + bool Connected(ChatProvider providerType); + + /// + /// Reconnect a specific to it's chat service + /// + /// The type of provider to reconnect + /// on success, error message on failure + [OperationContract] + string Reconnect(ChatProvider providerType); + } +} diff --git a/TGServiceInterface/Components/Compiler.cs b/TGServiceInterface/Components/Compiler.cs new file mode 100644 index 0000000000..f5b261fb62 --- /dev/null +++ b/TGServiceInterface/Components/Compiler.cs @@ -0,0 +1,61 @@ +using System.ServiceModel; + +namespace TGServiceInterface.Components +{ + /// + /// For managing the Game A/B/Live folders, compiling, and hotswapping them + /// + [ServiceContract] + public interface ITGCompiler + { + /// + /// Sets up the symlinks for hotswapping game code. This will reset everything in the Game folder. Requires the repository to be set up and locks it once the compilation stage starts. Runs asyncronously from this call + /// + /// if the operation began, if it could not start + [OperationContract] + 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 + /// + /// If no message for compilation start will be printed + /// if the operation began, if it could not start + [OperationContract] + bool Compile(bool silent = false); + + /// + /// Cancels the current compilation + /// + /// on success, error message on failure + [OperationContract] + string Cancel(); + + /// + /// Returns the current compiler status + /// + /// The current compiler status + [OperationContract] + CompilerStatus GetStatus(); + + /// + /// Returns the error message of the last operation. Reading this will clear the stored value + /// + /// the error message of the last operation if it failed or if it succeeded + [OperationContract] + string CompileError(); + + /// + /// Returns the relative path of the dme the compiler will look for without the .dme part + /// + /// The relative path of the dme the compiler will look for without the .dme part + [OperationContract] + string ProjectName(); + + /// + /// Sets the relative path of the dme the compiler will look for without the .dme part + /// + /// The relative path of the dme the compiler will look for without the .dme part + [OperationContract] + void SetProjectName(string projectName); + } +} 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 93% rename from TGServiceInterface/Connectivity.cs rename to TGServiceInterface/Components/Connectivity.cs index cd9424c61c..81975e6f84 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/Components/DreamDaemon.cs b/TGServiceInterface/Components/DreamDaemon.cs new file mode 100644 index 0000000000..b21fdda493 --- /dev/null +++ b/TGServiceInterface/Components/DreamDaemon.cs @@ -0,0 +1,146 @@ +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 53% rename from TGServiceInterface/Repository.cs rename to TGServiceInterface/Components/Repository.cs index eab2a89a8d..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 PullRequestInfo - /// - /// 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 /// @@ -55,32 +12,30 @@ namespace TGServiceInterface /// /// If the repo is currently undergoing an operation /// - /// true if the repo is busy, false otherwise + /// if the repo is busy, otherwise [OperationContract] bool OperationInProgress(); /// /// Gets the progress of repository operations, not all operations are supported /// - /// A value between 0 and 100 representing the progress of the current operation or -1 if the operation cannot be monitored + /// A value between 0 and 100 inclusive representing the progress of the current operation or -1 if the operation cannot be monitored [OperationContract] int CheckoutProgress(); /// - /// Check if the repository is valid, if not Setup must be called + /// Check if the repository is valid, if not must be called /// - /// true if the repository is valid, false otherwise + /// if the repository is valid, otherwise [OperationContract] bool Exists(); /// - /// Deletes whatever may be left over and clones the repo at remote and checks out branch master - /// Will move config and data dirs to a backup location if they exist - /// runs asyncronously + /// Deletes whatever may be left over and clones the repo at and checks out . Will move config and data dirs to a backup location if they exist. Runs asyncronously /// - /// The address of the repo to clone. If ssh protocol is used, repository_private_key.txt must exist in the server directory. + /// The address of the repo to clone. If ssh protocol is used, private_key.txt and public_key.txt must exist in the server RepoKey directory. /// The branch of the repo to checkout - /// null on success, error message on failure + /// on success, error message on failure [OperationContract] string Setup(string remote, string branch = "master"); @@ -88,48 +43,48 @@ namespace TGServiceInterface /// Gets the sha of the current HEAD /// /// If set to true and HEAD is currently a branch, will instead return the sha of the tracked remote branch if it exists - /// null on success, error message on failure - /// The sha of the current HEAD on success, null on failure + /// on success, error message on failure + /// The sha of the current HEAD on success, on failure [OperationContract] string GetHead(bool useTracked, out string error); /// /// Gets the name of the current branch /// - /// null on success, error message on failure - /// The name of the current branch on success, null on failure + /// on success, error message on failure + /// The name of the current branch on success, on failure [OperationContract] string GetBranch(out string error); /// /// Gets the url of the current origin /// - /// null on success, error message on failure - /// The url of the current origin on success, null on failure + /// on success, error message on failure + /// The url of the current origin on success, on failure [OperationContract] string GetRemote(out string error); /// /// Hard checks out the passed object name /// - /// The branch, commit, or tag to checkout - /// null on success, error message on failure + /// The branch, commit, or tag to checkout + /// on success, error message on failure [OperationContract] string Checkout(string objectName); /// /// Fetches the origin and merges it into the current branch /// - /// If true, the operation will perform a hard reset instead of a merge - /// null on success, error message on failure + /// If , the operation will perform a hard reset instead of a merge + /// on success, error message on failure [OperationContract] string Update(bool reset); /// /// Runs git reset --hard /// - /// Changes command to git reset --hard origin/branch_name if true - /// null on success, error message on failure + /// Changes command to git reset --hard origin/branch_name if + /// on success, error message on failure [OperationContract] string Reset(bool tracked); @@ -137,18 +92,15 @@ namespace TGServiceInterface /// Merges the target pull request into the current branch if the remote is a github repository /// /// The github pull request number in the remote repository - /// null on success, error message on failure + /// on success, error message on failure [OperationContract] string MergePullRequest(int PRnumber); - //Returns a list of PR# -> Sha of the currently merged pull requests - //returns null on failure and error will be set - /// /// Get the currently merged pull requests. Note that switching branches will delete this list and switching back won't restore it /// - /// null on success, error message on failure - /// A list of PullRequestInfo + /// on success, error message on failure + /// A of [OperationContract] IList MergedPullRequests(out string error); @@ -183,23 +135,23 @@ namespace TGServiceInterface /// /// Updates the html changelog /// - /// null on success, error on failure + /// on success, error on failure /// The output of the python script [OperationContract] string GenerateChangelog(out string error); - /// - /// Pushes the changelog to the currently git, this operation will only run if the changelog is the only difference to be pushed (i.e. no PRs merged) - /// - /// null on success, error on failure - [OperationContract] - string PushChangelog(); + /// + /// Pushes the paths listed in TGS3.json to the currentl git remote. No other commit differences may exist for this function to succeed + /// + /// on success, error on failure + [OperationContract] + string SynchronizePush(); /// /// Sets the path to the python 2.7 installation /// /// The new path - /// true if the path exists, false otherwise + /// if the path exists, otherwise [OperationContract] bool SetPythonPath(string path); @@ -213,30 +165,29 @@ namespace TGServiceInterface /// /// List the tagged commits of the repo at which compiles took place /// - /// null on success, error message on failure - /// A dictionary of tag name -> commit on success, null on failure + /// on success, error message on failure + /// A of tag name -> commit on success, on failure [OperationContract] IDictionary ListBackups(out string error); /// - /// Updates the cached TGS3.json to the repo's version - /// Compiles will not succeed if these two to not match + /// Updates the cached TGS3.json to the repo's version. Compiles will not succeed if these two to not match /// - /// null on success, error message on failure + /// on success, error message on failure [OperationContract] string UpdateTGS3Json(); /// /// (De)Activate and set the interval for the automatic server updater /// - /// Interval to check for updates in minutes, disables if zero + /// Interval to check for updates in minutes, disables if 0 [OperationContract] void SetAutoUpdateInterval(ulong newInterval); /// /// Get the current autoupdate interval /// - /// The current auto update interval or zero if it's disabled + /// The current auto update interval or 0 if it's disabled [OperationContract] ulong AutoUpdateInterval(); } diff --git a/TGServiceInterface/Service.cs b/TGServiceInterface/Components/Service.cs similarity index 98% rename from TGServiceInterface/Service.cs rename to TGServiceInterface/Components/Service.cs index a03e6710ea..04a300827a 100644 --- a/TGServiceInterface/Service.cs +++ b/TGServiceInterface/Components/Service.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ServiceModel; -namespace TGServiceInterface +namespace TGServiceInterface.Components { /// /// Interface for managing the service diff --git a/TGServiceInterface/DreamDaemon.cs b/TGServiceInterface/DreamDaemon.cs deleted file mode 100644 index 6162ad3ae8..0000000000 --- a/TGServiceInterface/DreamDaemon.cs +++ /dev/null @@ -1,189 +0,0 @@ -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 TGDreamDaemonStatus - [OperationContract] - TGDreamDaemonStatus DaemonStatus(); - - /// - /// Returns a human readable string of the current server status - /// - /// If true, the status will include the server's current visibility and security levels - /// A human readable string of the current server status - [OperationContract] - string StatusString(bool includeMetaInfo); - - /// - /// Check if a call to Start will fail - /// Of course, be aware of race conditions with other control panels - /// - /// returns the error that would occur, null otherwise - [OperationContract] - string CanStart(); - - /// - /// Starts the server if it isn't running - /// - /// null on success or error message on failure - [OperationContract] - string Start(); - - /// - /// Immediately kills the server - /// - /// null on success or error message on failure - [OperationContract] - string Stop(); - - /// - /// Immediately kills and restarts the server - /// - /// null on success or error message on failure - [OperationContract] - string Restart(); - - /// - /// Restart the server after the currently running round ends - /// Has no effect if the server isn't running - /// - [OperationContract] - void RequestRestart(); - - /// - /// Stop the server after the currently running round ends - /// Has no effect if the server isn't running - /// - [OperationContract] - void RequestStop(); - - /// - /// Get the configured (not running) security level - /// - /// The configured (not running) security level - [OperationContract] - TGDreamDaemonSecurity SecurityLevel(); - - /// - /// Sets the security level of the server. Requires reboot to apply - /// Implies a call to RequestRestart() - /// note that anything higher than Trusted will disable interop from DD - /// - /// The new security level - /// True if the change was immediately applied, false if a graceful restart was queued - [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 - /// Implies a call to RequestRestart() - /// - /// The new port - [OperationContract] - void SetPort(ushort new_port); - - /// - /// Check if the watchdog will start when the service starts - /// - /// true if autostart is enabled, false otherwise - [OperationContract] - bool Autostart(); - - /// - /// Set the autostart config - /// - /// true to start the watchdog with the service, false otherwise - [OperationContract] - void SetAutostart(bool on); - - /// - /// Check if the byond webclient is currently enabled for the server - /// - /// true if the webclient is enabled, false otherwise - [OperationContract] - bool Webclient(); - - /// - /// Set the webclient config. Calls - /// - /// true to enable the byond webclient for the server, false otherwise - [OperationContract] - void SetWebclient(bool on); - - /// - /// Checks if a server stop has bee requested - /// - /// true if RequestStop has been called since the last server start, false otherwise - [OperationContract] - bool ShutdownInProgress(); - - /// - /// Sends a message to everyone on the server - /// - /// The message to send - /// null 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/DreamDaemonBridge.cs b/TGServiceInterface/DreamDaemonBridge.cs new file mode 100644 index 0000000000..a85ae6541b --- /dev/null +++ b/TGServiceInterface/DreamDaemonBridge.cs @@ -0,0 +1,36 @@ +using RGiesecke.DllExport; +using System; +using System.Runtime.InteropServices; +using TGServiceInterface.Components; + +namespace TGServiceInterface +{ + /// + /// Holds the proc that DD calls to access + /// + public sealed class DreamDaemonBridge + { + /// + /// The proc that DD calls to access + /// + /// The number of arguments passed + /// The arguments passed + /// 0 + [DllExport("DDEntryPoint", CallingConvention = CallingConvention.Cdecl)] + public static int DDEntryPoint(int argc, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeParamIndex = 0)]string[] args) + { + try + { + var channel = Interface.CreateChannel(); + try + { + channel.CreateChannel().InteropMessage(String.Join(" ", args)); + } + catch { } + Interface.CloseChannel(channel); + } + catch { } + return 0; + } + } +} 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/Helpers.cs b/TGServiceInterface/Helpers.cs index 6d8e3e0f70..471d84ccfd 100644 --- a/TGServiceInterface/Helpers.cs +++ b/TGServiceInterface/Helpers.cs @@ -4,26 +4,41 @@ using System.Text; namespace TGServiceInterface { + /// + /// Helper functions used across the server suite + /// public static class Helpers { - public static string EncryptData(string data, out string sentropy) + /// + /// Takes some and returns an encrypted version along with the required to decrypt it + /// + /// The to encrypt + /// The entropy required the decrypt the ciphertext + /// Ciphertext for the + public static string EncryptData(string cleartext, out string entropy) { // Generate additional entropy (will be used as the Initialization vector) - byte[] entropy = new byte[20]; + byte[] bentropy = new byte[20]; using (var rng = new RNGCryptoServiceProvider()) - rng.GetBytes(entropy); + rng.GetBytes(bentropy); - byte[] ciphertext = ProtectedData.Protect(Encoding.UTF8.GetBytes(data), entropy, DataProtectionScope.CurrentUser); + byte[] ciphertext = ProtectedData.Protect(Encoding.UTF8.GetBytes(cleartext), bentropy, DataProtectionScope.CurrentUser); - sentropy = Convert.ToBase64String(entropy, 0, entropy.Length); + entropy = Convert.ToBase64String(bentropy, 0, bentropy.Length); return Convert.ToBase64String(ciphertext, 0, ciphertext.Length); } - public static string DecryptData(string data, string entropy) + /// + /// Takes ciphertext and entropy from and returns the cleartext. Note that this only works if the OS user of the program is the same one that called + /// + /// A return value from a previous call to + /// The entropy parameter from the previous call to that returned + /// The decrypted on sucess or on failure + public static string DecryptData(string ciphertext, string entropy) { try { - return Encoding.UTF8.GetString(ProtectedData.Unprotect(Convert.FromBase64String(data), Convert.FromBase64String(entropy), DataProtectionScope.CurrentUser)); + return Encoding.UTF8.GetString(ProtectedData.Unprotect(Convert.FromBase64String(ciphertext), Convert.FromBase64String(entropy), DataProtectionScope.CurrentUser)); } catch { diff --git a/TGServiceInterface/Interface.cs b/TGServiceInterface/Interface.cs index eb19260f46..1cd73b4242 100644 --- a/TGServiceInterface/Interface.cs +++ b/TGServiceInterface/Interface.cs @@ -1,34 +1,41 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Net; using System.Net.Security; +using System.Reflection; using System.Security.Principal; using System.ServiceModel; +using TGServiceInterface.Components; + namespace TGServiceInterface { - public class Server + /// + /// Main inteface class for the service + /// + public class Interface { /// - /// List of types that can be used with GetComponen + /// List of s that can be used with and /// - public static readonly IList InstanceInterfaces = new List { typeof(ITGByond), typeof(ITGChat), typeof(ITGCompiler), typeof(ITGConfig), typeof(ITGDreamDaemon), typeof(ITGRepository), typeof(ITGSService), typeof(ITGConnectivity), typeof(ITGAdministration), typeof(ITGInterop) }; + public static readonly IList ValidInterfaces = CollectComponents(); /// /// The maximum message size to and from a local server /// - public static readonly long TransferLimitLocal = Int32.MaxValue; //2GB can't go higher + public const long TransferLimitLocal = Int32.MaxValue; //2GB can't go higher /// /// The maximum message size to and from a remote server /// - public static readonly long TransferLimitRemote = 10485760; //10 MB + public const long TransferLimitRemote = 10485760; //10 MB /// /// Base name of the communication pipe /// they are formatted as MasterPipeName/ComponentName /// - public static string MasterInterfaceName = "TGStationServerService"; + public const string MasterInterfaceName = "TGStationServerService"; /// /// If this is set, we will try and connect to an HTTPS server running at this address @@ -50,7 +57,30 @@ namespace TGServiceInterface /// static string HTTPSPassword; - static Dictionary ChannelFactoryCache = new Dictionary(); + /// + /// 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 IDictionary ChannelFactoryCache = new Dictionary(); + + /// + /// Returns a of s that can be used with the service + /// + /// A of s that can be used with the service + static IList CollectComponents() + { + //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(ITGSService).Namespace + && t.GetCustomAttribute(typeof(ServiceContractAttribute)) != null + select t; + return query.ToList(); + } + + /// + /// Sets the function called when a remote login fails due to the server having an invalid SSL cert + /// + /// The to be called when a remote login is attempted while the server posesses a bad certificate. Passed a of error information about the and should return if it the connection should be made anyway public static void SetBadCertificateHandler(Func handler) { ServicePointManager.ServerCertificateValidationCallback = (sender, cert, chain, error) => @@ -88,16 +118,27 @@ namespace TGServiceInterface ClearCachedChannels(); } + /// + /// Closes all s stored in and clears it + /// static void ClearCachedChannels() { - foreach (var I in ChannelFactoryCache) - CloseChannel(I.Value); - ChannelFactoryCache.Clear(); + lock (ChannelFactoryCache) + { + foreach (var I in ChannelFactoryCache) + CloseChannel(I.Value); + ChannelFactoryCache.Clear(); + } } + /// + /// Returns if the interface being used to connect to a service does not have the same release version as the service + /// + /// An error message to display to the user should this function return + /// if the interface being used to connect to a service does not have the same release version as the service public static bool VersionMismatch(out string errorMessage) { - var splits = Server.GetComponent().Version().Split(' '); + var splits = GetComponent().Version().Split(' '); var theirs = new Version(splits[splits.Length - 1].Substring(1)); var ours = new Version(FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).FileVersion); if(theirs != ours) @@ -110,10 +151,12 @@ namespace TGServiceInterface } /// - /// Set the interface to look for services on a remote computer + /// Set the remote to connect to along with /// - /// - /// + /// The address of the remote server + /// The port the remote server runs on + /// Windows account username for the remote server + /// Windows account password for the remote server public static void SetRemoteLoginInformation(string address, ushort port, string username, string password) { HTTPSURL = address; @@ -123,6 +166,10 @@ namespace TGServiceInterface ClearCachedChannels(); } + /// + /// Safely shuts down a single + /// + /// The to shutdown public static void CloseChannel(ChannelFactory cf) { try @@ -136,10 +183,10 @@ namespace TGServiceInterface } /// - /// Returns the requested server component interface. This does not guarantee a successful connection + /// Returns the requested component . This does not guarantee a successful connection. s created this way are recycled for minimum latency and bandwidth usage /// - /// The type of the component to retrieve - /// The correct component + /// The component to retrieve + /// The correct component public static T GetComponent() { var tot = typeof(T); @@ -166,10 +213,16 @@ namespace TGServiceInterface return cf.CreateChannel(); } + /// + /// Directly creates a for without caching. This should be eventually closed by the caller + /// + /// The component of the channel to be created + /// The correct + /// Thrown if isn't a valid component public static ChannelFactory CreateChannel() { var ToT = typeof(T); - if (!InstanceInterfaces.Contains(ToT) && ToT != typeof(ITGSService)) + if (!ValidInterfaces.Contains(ToT) && ToT != typeof(ITGSService)) throw new Exception("Invalid type!"); var InterfaceName = typeof(T).Name; if (HTTPSURL == null) @@ -201,11 +254,9 @@ namespace TGServiceInterface } /// - /// Used to test if the service is avaiable on the machine - /// Note that state can technically change at any time - /// and any call to the service may throw an exception because it failed + /// Used to test if the service is avaiable on the machine. Note that state can technically change at any time and any call to the service may throw an exception because it failed /// - /// null on successful connection, error message on failure + /// on successful connection, error message on failure public static string VerifyConnection() { try @@ -220,10 +271,9 @@ namespace TGServiceInterface } /// - /// As opposed to VerifyConnection(), this check user credentials - /// Requires a prior call to + /// Checks if the supplied user's credentials have permission to use the service. Requires a successful prior call to /// - /// true if credentials are valid, false otherwise + /// if credentials are valid, otherwise public static bool Authenticate() { try @@ -238,10 +288,9 @@ namespace TGServiceInterface } /// - /// As opposed to Authentication() this returns true if the current login can use the interface. - /// Requires a prior call to + /// Checks if the current login can use . Requires a successful prior call to /// - /// true if the connection may use the interface, false otherwise + /// if the connection may use , otherwise public static bool AuthenticateAdmin() { try diff --git a/TGServiceInterface/Interop.cs b/TGServiceInterface/Interop.cs deleted file mode 100644 index 57e0eb3e69..0000000000 --- a/TGServiceInterface/Interop.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using RGiesecke.DllExport; -using System.Runtime.InteropServices; -using System.ServiceModel; - -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 - /// true on success, false on failure - [OperationContract] - bool InteropMessage(string command); - } - - /// - /// Holds the proc that DD calls to access - /// - public class DDInteropCallHolder - { - /// - /// The proc that DD calls to access - /// - /// The arguments passed - /// 0 - [DllExport("DDEntryPoint", CallingConvention = CallingConvention.Cdecl)] - public static int DDEntryPoint(int argc, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeParamIndex = 0)]string[] args) - { - try - { - var channel = Interface.CreateChannel(); - try - { - channel.CreateChannel().InteropMessage(String.Join(" ", args)); - } - catch { } - Interface.CloseChannel(channel); - - } - catch { } - return 0; - } - } -} 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 new file mode 100644 index 0000000000..90b26f352e --- /dev/null +++ b/TGServiceInterface/RootCommand.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; + +namespace TGServiceInterface +{ + /// + /// Helper for creating commands that contain sub commands + /// + public abstract class RootCommand : Command + { + /// + /// s further down the tree from this one. Set in Constructor + /// + public Command[] Children { get; protected set; } = { }; + /// + /// If set to a multiline, detailed list of s will be printed. Otherwise a singleline list of s will be printed + /// + public static bool PrintHelpList = false; + + /// + /// 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 + protected override ExitCode Run(IList parameters) + { + if (parameters.Count > 0) + { + var LocalKeyword = parameters[0].Trim().ToLower(); + parameters.RemoveAt(0); + switch (LocalKeyword) + { + case "help": + case "?": + PrintHelp(); + return ExitCode.Normal; + default: + foreach (var c in Children) + if (c.Keyword == LocalKeyword) + { + if (parameters.Count > 0) + { + var possibleHelp = parameters[0].ToLower(); + if (possibleHelp == "help" || possibleHelp == "?") + { + c.PrintHelp(); + return ExitCode.Normal; + } + } + if (parameters.Count < c.RequiredParameters) + { + OutputProc("Not enough parameters!"); + return ExitCode.BadCommand; + } + return c.DoRun(parameters); + } + parameters.Insert(0, LocalKeyword); + break; + } + } + OutputProc(String.Format("Invalid command! Type '{0}?' or '{0}help' for available commands.", Keyword != null ? Keyword + " " : "")); + return ExitCode.BadCommand; + } + + /// + public override void PrintHelp() + { + var Final = new List(); + if (PrintHelpList) + { + foreach (var c in Children) + Final.Add(c.Keyword); + OutputProc("Available commands (type '?' or 'help' after command for more info): " + String.Join(", ", Final)); + } + else + { + var Prefixes = new List(); + var Postfixes = new List(); + int MaxPrefixLen = 0; + foreach (var c in Children) + { + var ns = c.Keyword + " " + c.GetArgumentString(); + MaxPrefixLen = Math.Max(MaxPrefixLen, ns.Length); + Prefixes.Add(ns); + Postfixes.Add(c.GetHelpText()); + } + + for (var I = 0; I < Prefixes.Count; ++I) + { + var lp = Prefixes[I]; + for (; lp.Length < MaxPrefixLen + 1; lp += " ") ; + Final.Add(lp + "- " + Postfixes[I]); + } + Final.Sort(); + Final.ForEach(OutputProc); + } + } + + /// + public override string GetHelpText() + { + throw new NotImplementedException(); + } + } +} diff --git a/TGServiceInterface/TGServiceInterface.csproj b/TGServiceInterface/TGServiceInterface.csproj index c7beb2c621..ea546a5b8a 100644 --- a/TGServiceInterface/TGServiceInterface.csproj +++ b/TGServiceInterface/TGServiceInterface.csproj @@ -32,6 +32,8 @@ x86 prompt MinimumRecommendedRules.ruleset + true + bin\x86\Release\TGServiceInterface.xml @@ -45,21 +47,26 @@ - - + + + - - - - - + + + + + + + - + + + - - + + diff --git a/TGStationServer3.sln b/TGStationServer3.sln index 48358be2aa..346a49d8d1 100644 --- a/TGStationServer3.sln +++ b/TGStationServer3.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26730.3 +VisualStudioVersion = 15.0.26730.16 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TGServerService", "TGServerService\TGServerService.csproj", "{F32EDA25-0855-411C-AF5E-F0D042917E2D}" EndProject @@ -18,20 +18,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .gitignore = .gitignore .travis.yml = .travis.yml appveyor.yml = appveyor.yml - build_byond.sh = build_byond.sh - Config.dm = Config.dm - DMAPITravisTester.dme = DMAPITravisTester.dme - install_byond.sh = install_byond.sh README.md = README.md - Test.dm = Test.dm tgs.ico = tgs.ico - TGS3Release.ps1 = TGS3Release.ps1 Version.cs = Version.cs - View TGS3 Logs.xml = View TGS3 Logs.xml EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TGInstallerWrapper", "TGInstallerWrapper\TGInstallerWrapper.csproj", "{8956D4C3-BFB9-448E-BF5F-EE7E6F9996F9}" ProjectSection(ProjectDependencies) = postProject + {AC4E7E8B-F83A-481C-A8B0-8FA4E8AE59AB} = {AC4E7E8B-F83A-481C-A8B0-8FA4E8AE59AB} {154435F6-0890-42D4-9AEC-B743D4FBC1CB} = {154435F6-0890-42D4-9AEC-B743D4FBC1CB} EndProjectSection EndProject @@ -76,36 +70,50 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DMAPI", "DMAPI", "{9032B448 TGS3.json = TGS3.json EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{081BB0BB-2E84-47D4-9419-A2AB8E814ABF}" + ProjectSection(SolutionItems) = preProject + Tools\build_byond.sh = Tools\build_byond.sh + Tools\Config.dm = Tools\Config.dm + Tools\DMAPITravisTester.dme = Tools\DMAPITravisTester.dme + Tools\Doxyfile = Tools\Doxyfile + Tools\install_byond.sh = Tools\install_byond.sh + Tools\Test.dm = Tools\Test.dm + Tools\TGS3Build.ps1 = Tools\TGS3Build.ps1 + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{287B900C-1AFB-49B8-8BDD-C9058971C20C}" + ProjectSection(SolutionItems) = preProject + .github\CONTRIBUTING.md = .github\CONTRIBUTING.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F32EDA25-0855-411C-AF5E-F0D042917E2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F32EDA25-0855-411C-AF5E-F0D042917E2D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F32EDA25-0855-411C-AF5E-F0D042917E2D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F32EDA25-0855-411C-AF5E-F0D042917E2D}.Release|Any CPU.Build.0 = Release|Any CPU - {394E7643-6B8C-416F-AB18-95AC12648CDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {394E7643-6B8C-416F-AB18-95AC12648CDC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {394E7643-6B8C-416F-AB18-95AC12648CDC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {394E7643-6B8C-416F-AB18-95AC12648CDC}.Release|Any CPU.Build.0 = Release|Any CPU - {89191F69-B18E-4B59-B72E-E12F9B6811A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {89191F69-B18E-4B59-B72E-E12F9B6811A0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {89191F69-B18E-4B59-B72E-E12F9B6811A0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {89191F69-B18E-4B59-B72E-E12F9B6811A0}.Release|Any CPU.Build.0 = Release|Any CPU - {AC4E7E8B-F83A-481C-A8B0-8FA4E8AE59AB}.Debug|Any CPU.ActiveCfg = Debug|x86 - {AC4E7E8B-F83A-481C-A8B0-8FA4E8AE59AB}.Debug|Any CPU.Build.0 = Debug|x86 - {AC4E7E8B-F83A-481C-A8B0-8FA4E8AE59AB}.Release|Any CPU.ActiveCfg = Release|x86 - {AC4E7E8B-F83A-481C-A8B0-8FA4E8AE59AB}.Release|Any CPU.Build.0 = Release|x86 - {154435F6-0890-42D4-9AEC-B743D4FBC1CB}.Debug|Any CPU.ActiveCfg = Debug|x86 - {154435F6-0890-42D4-9AEC-B743D4FBC1CB}.Debug|Any CPU.Build.0 = Debug|x86 - {154435F6-0890-42D4-9AEC-B743D4FBC1CB}.Release|Any CPU.ActiveCfg = Release|x86 - {154435F6-0890-42D4-9AEC-B743D4FBC1CB}.Release|Any CPU.Build.0 = Release|x86 - {8956D4C3-BFB9-448E-BF5F-EE7E6F9996F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8956D4C3-BFB9-448E-BF5F-EE7E6F9996F9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8956D4C3-BFB9-448E-BF5F-EE7E6F9996F9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8956D4C3-BFB9-448E-BF5F-EE7E6F9996F9}.Release|Any CPU.Build.0 = Release|Any CPU + {F32EDA25-0855-411C-AF5E-F0D042917E2D}.Debug|x86.ActiveCfg = Debug|x86 + {F32EDA25-0855-411C-AF5E-F0D042917E2D}.Debug|x86.Build.0 = Debug|x86 + {F32EDA25-0855-411C-AF5E-F0D042917E2D}.Release|x86.ActiveCfg = Release|x86 + {F32EDA25-0855-411C-AF5E-F0D042917E2D}.Release|x86.Build.0 = Release|x86 + {394E7643-6B8C-416F-AB18-95AC12648CDC}.Debug|x86.ActiveCfg = Debug|x86 + {394E7643-6B8C-416F-AB18-95AC12648CDC}.Debug|x86.Build.0 = Debug|x86 + {394E7643-6B8C-416F-AB18-95AC12648CDC}.Release|x86.ActiveCfg = Release|x86 + {394E7643-6B8C-416F-AB18-95AC12648CDC}.Release|x86.Build.0 = Release|x86 + {89191F69-B18E-4B59-B72E-E12F9B6811A0}.Debug|x86.ActiveCfg = Debug|x86 + {89191F69-B18E-4B59-B72E-E12F9B6811A0}.Debug|x86.Build.0 = Debug|x86 + {89191F69-B18E-4B59-B72E-E12F9B6811A0}.Release|x86.ActiveCfg = Release|x86 + {89191F69-B18E-4B59-B72E-E12F9B6811A0}.Release|x86.Build.0 = Release|x86 + {AC4E7E8B-F83A-481C-A8B0-8FA4E8AE59AB}.Debug|x86.ActiveCfg = Debug|x86 + {AC4E7E8B-F83A-481C-A8B0-8FA4E8AE59AB}.Debug|x86.Build.0 = Debug|x86 + {AC4E7E8B-F83A-481C-A8B0-8FA4E8AE59AB}.Release|x86.ActiveCfg = Release|x86 + {AC4E7E8B-F83A-481C-A8B0-8FA4E8AE59AB}.Release|x86.Build.0 = Release|x86 + {154435F6-0890-42D4-9AEC-B743D4FBC1CB}.Debug|x86.ActiveCfg = Debug|x86 + {154435F6-0890-42D4-9AEC-B743D4FBC1CB}.Release|x86.ActiveCfg = Release|x86 + {154435F6-0890-42D4-9AEC-B743D4FBC1CB}.Release|x86.Build.0 = Release|x86 + {8956D4C3-BFB9-448E-BF5F-EE7E6F9996F9}.Debug|x86.ActiveCfg = Debug|x86 + {8956D4C3-BFB9-448E-BF5F-EE7E6F9996F9}.Release|x86.ActiveCfg = Release|x86 + {8956D4C3-BFB9-448E-BF5F-EE7E6F9996F9}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Config.dm b/Tools/Config.dm similarity index 100% rename from Config.dm rename to Tools/Config.dm diff --git a/Tools/DMAPITravisTester.dme b/Tools/DMAPITravisTester.dme new file mode 100644 index 0000000000..dd468a6dfe --- /dev/null +++ b/Tools/DMAPITravisTester.dme @@ -0,0 +1,19 @@ +// Hand crafted DME, will not work if saved with DreamMaker + +// BEGIN_INTERNALS +// END_INTERNALS + +// BEGIN_FILE_DIR +#define FILE_DIR . +// END_FILE_DIR + +// BEGIN_PREFERENCES +// END_PREFERENCES + +// BEGIN_INCLUDE +#include "Config.dm" +#include "..\DMAPI\server_tools.dm" +#include "..\DMAPI\st_commands.dm" +#include "..\DMAPI\st_interface.dm" +#include "Test.dm" +// END_INCLUDE diff --git a/Tools/Doxyfile b/Tools/Doxyfile new file mode 100644 index 0000000000..2cab89ff8e --- /dev/null +++ b/Tools/Doxyfile @@ -0,0 +1,2479 @@ +# Doxyfile 1.8.13 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file +# that follow. The default is UTF-8 which is also the encoding used for all text +# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv +# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv +# for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = tgstation-server + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +# Generated in appveyor + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = "The /tg/station 13 server suite" + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +# Generated in appveyor + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +# Generated in appveyor + +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines. + +ALIASES = + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding "class=itcl::class" +# will allow you to use the command class in the itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = YES + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, Javascript, +# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran: +# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran: +# Fortran. In the later case the parser tries to guess whether the code is fixed +# or free formatted code, this is the default for Fortran type files), VHDL. For +# instance to make doxygen treat .inc files as Fortran files (default is PHP), +# and .f files as C (default is Fortran), use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See http://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = YES + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = YES + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = YES + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# (class|struct|union) declarations. If set to NO, these declarations will be +# included in the documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file +# names in lower-case letters. If set to YES, upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. +# The default value is: system dependent. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = NO + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong or incomplete +# parameter documentation, but not about the absence of documentation. +# The default value is: NO. + +WARN_NO_PARAMDOC = YES + +# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# a warning is encountered. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +# Generated by appveyor + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: http://www.gnu.org/software/libiconv) for the list of +# possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by doxygen. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, +# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, +# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, +# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf and *.qsf. + +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.idl \ + *.ddl \ + *.odl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.cs \ + *.d \ + *.php \ + *.php4 \ + *.php5 \ + *.phtml \ + *.inc \ + *.m \ + *.markdown \ + *.md \ + *.mm \ + *.dox \ + *.py \ + *.pyw \ + *.f90 \ + *.f95 \ + *.f03 \ + *.f08 \ + *.f \ + *.for \ + *.tcl \ + *.vhd \ + *.vhdl \ + *.ucf \ + *.qsf + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +#We want the main .md +EXCLUDE = legacy/README.md packages + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = README.md + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = YES + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = YES + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# function all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = YES + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = YES + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see http://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the config file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the +# clang parser (see: http://clang.llvm.org/) for more accurate parsing at the +# cost of reduced performance. This can be particularly helpful with template +# rich C++ code for which doxygen's built-in parser lacks the necessary type +# information. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse-libclang=ON option for CMake. +# The default value is: NO. + +CLANG_ASSISTED_PARSING = NO + +# If clang assisted parsing is enabled you can provide the compiler with command +# line options that you would normally use when invoking the compiler. Note that +# the include paths will already be set by doxygen for the files and directories +# specified with INPUT and INCLUDE_PATH. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_OPTIONS = + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in +# which the alphabetical index list will be split. +# Minimum value: 1, maximum value: 20, default value: 5. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = . + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# http://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to YES can help to show when doxygen was last run and thus if the +# documentation is up to date. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = NO + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: http://developer.apple.com/tools/xcode/), introduced with +# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a +# Makefile in the HTML output directory. Running make will produce the docset in +# that directory and running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the master .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual- +# folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location of Qt's +# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the +# generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# http://www.mathjax.org) which uses client side Javascript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from http://www.mathjax.org before deployment. +# The default value is: http://cdn.mathjax.org/mathjax/latest. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /