diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000000..369dd46cdc --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,168 @@ +# 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. Do not write a constructor for the Service classas Windows will not let you debug it properly + +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. For reference here is the version format we use 3.\.\.\ The criteria for changing a version number is as follows + +- Major: A breaking change to the DMAPI +- Minor: Additions or changes to the interface or DMAPI +- Patch: Non-breaking changes internal to each of the 3 modules (Service, Interface, DMAPI) + +### 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. + +## Other Notes + +* Feel free to add your nuget account to TGServiceInterface/Packages.nuspec authors list if you modify the interface \ No newline at end of file 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/DMAPI/server_tools.dm b/DMAPI/server_tools.dm index 88daeb3667..f9e5024933 100644 --- a/DMAPI/server_tools.dm +++ b/DMAPI/server_tools.dm @@ -1,4 +1,5 @@ -// /tg/station 13 server tools API v3.1.0.2 +// /tg/station 13 server tools API +#define SERVICE_API_VERSION_STRING "3.2.0.0" //CONFIGURATION //use this define if you want to do configuration outside of this file @@ -64,16 +65,15 @@ //IMPLEMENTATION -#define SERVICE_API_VERSION_STRING "3.1.0.2" - #define REBOOT_MODE_NORMAL 0 #define REBOOT_MODE_HARD 1 #define REBOOT_MODE_SHUTDOWN 2 #define SERVICE_WORLD_PARAM "server_service" #define SERVICE_VERSION_PARAM "server_service_version" +#define SERVICE_INSTANCE_PARAM "server_instance" #define SERVICE_PR_TEST_JSON "prtestjob.json" -#define SERVICE_INTERFACE_DLL "TGServiceInterface.dll" +#define SERVICE_INTERFACE_DLL "TGDreamDaemonBridge.dll" #define SERVICE_INTERFACE_FUNCTION "DDEntryPoint" #define SERVICE_CMD_HARD_REBOOT "hard_reboot" @@ -98,6 +98,8 @@ #define SERVICE_REQUEST_WORLD_REBOOT "worldreboot" #define SERVICE_REQUEST_API_VERSION "api_ver" +#define SERVICE_RETURN_SUCCESS "SUCCESS" + /* The MIT License diff --git a/DMAPI/st_interface.dm b/DMAPI/st_interface.dm index 07e6a40bff..61aabcebce 100644 --- a/DMAPI/st_interface.dm +++ b/DMAPI/st_interface.dm @@ -30,7 +30,7 @@ SERVER_TOOLS_DEFINE_AND_SET_GLOBAL(server_tools_api_compatible, FALSE) return if(skip_compat_check && !fexists(SERVICE_INTERFACE_DLL)) CRASH("Service parameter present but no interface DLL detected. This is symptomatic of running a service less than version 3.1! Please upgrade.") - call(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(command) //trust no retval + call(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(params[SERVICE_INSTANCE_PARAM], command) //trust no retval return TRUE /world/proc/ChatBroadcast(message) @@ -72,7 +72,7 @@ SERVER_TOOLS_DEFINE_AND_SET_GLOBAL(server_tools_api_compatible, FALSE) switch(command) if(SERVICE_CMD_API_COMPATIBLE) SERVER_TOOLS_WRITE_GLOBAL(server_tools_api_compatible, TRUE) - return "SUCCESS" + return SERVICE_RETURN_SUCCESS if(SERVICE_CMD_HARD_REBOOT) if(SERVER_TOOLS_READ_GLOBAL(reboot_mode) != REBOOT_MODE_HARD) SERVER_TOOLS_WRITE_GLOBAL(reboot_mode, REBOOT_MODE_HARD) @@ -88,7 +88,7 @@ SERVER_TOOLS_DEFINE_AND_SET_GLOBAL(server_tools_api_compatible, FALSE) if(!istext(msg) || !msg) return "No message set!" SERVER_TOOLS_WORLD_ANNOUNCE(msg) - return "SUCCESS" + return SERVICE_RETURN_SUCCESS if(SERVICE_CMD_PLAYER_COUNT) return "[SERVER_TOOLS_CLIENT_COUNT]" if(SERVICE_CMD_LIST_CUSTOM) @@ -96,7 +96,7 @@ SERVER_TOOLS_DEFINE_AND_SET_GLOBAL(server_tools_api_compatible, FALSE) else var/custom_command_result = HandleServiceCustomCommand(lowertext(command), params[SERVICE_CMD_PARAM_SENDER], params[SERVICE_CMD_PARAM_CUSTOM]) if(custom_command_result) - return istext(custom_command_result) ? custom_command_result : "SUCCESS" + return istext(custom_command_result) ? custom_command_result : SERVICE_RETURN_SUCCESS return "Unknown command: [command]" /* 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..3df7bca9b4 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,9 +73,10 @@ 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 + - Power shell users remember to quit the appid: `netsh http add sslcert ipport=0.0.0.0: certhash= appid="{F32EDA25-0855-411C-AF5E-F0D042917E2D}"` as {} has special meaning in powershell 1. Ensure the port can be acccessed from the internet 1. Log in from any computer using a username and password from the service computer in either the CLI or GUI @@ -97,6 +98,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. @@ -117,6 +121,15 @@ 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 + +* `Instance.cfg` + * The encrypted internal configuration settings for a server instance. Note that access to this file bypasses API user restrictions + +### Codebase integration +To get the TGS3 API for your code base, import the 3 .dm files in the `DMAPI` folder into your include structure, then fill out the configuration as documented in the comments of server_tools.dm. Then, you may want to add a TGS3.json file to specify any static directories and .dlls your codebase uses, along with the optional changelog compile options. ### Starting the game server: To run the game server, open the `Server` tab of the control panel and click either `Start` @@ -168,7 +181,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 +193,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..aedddd15b1 100644 --- a/TGCommandLine/AdminCommands.cs +++ b/TGCommandLine/AdminCommands.cs @@ -1,48 +1,23 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { - class AdminCommand : RootCommand + class AdminCommand : InstanceRootCommand { public AdminCommand() { Keyword = "admin"; - Children = new Command[] { new AdminViewGroupCommand(), new AdminSetGroupCommand(), new AdminClearGroupCommand(), new AdminViewPortCommand(), new AdminSetPortCommand(), new AdminMoveServerCommand(), new AdminRecreateStaticCommand() }; + Children = new Command[] { new AdminViewGroupCommand(), new AdminSetGroupCommand(), new AdminClearGroupCommand(), new AdminRecreateStaticCommand() }; } public override string GetHelpText() { - return "Manage server service authentication"; + return "Manage instance authentication"; } } - class AdminMoveServerCommand : Command - { - public AdminMoveServerCommand() - { - Keyword = "move-server"; - RequiredParameters = 1; - } - - protected override ExitCode Run(IList parameters) - { - var res = Server.GetComponent().MoveServer(parameters[0]); - OutputProc(res ?? "Success"); - return res == null ? ExitCode.Normal : ExitCode.ServerError; - } - - public override string GetArgumentString() - { - return ""; - } - - public override string GetHelpText() - { - return "Move the server installation (BYOND, Repo, Game) to a new location. Nothing else may be running for this task to complete"; - } - } - class AdminRecreateStaticCommand : Command + class AdminRecreateStaticCommand : ConsoleCommand { public AdminRecreateStaticCommand() { @@ -51,7 +26,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; } @@ -62,60 +37,7 @@ namespace TGCommandLine } } - class AdminSetPortCommand : Command - { - public AdminSetPortCommand() - { - Keyword = "set-port"; - RequiredParameters = 1; - } - public override string GetHelpText() - { - return "Set the port used for remote access. Requires a service restart to take effect"; - } - - public override string GetArgumentString() - { - return ""; - } - - protected override ExitCode Run(IList parameters) - { - ushort port; - try - { - port = Convert.ToUInt16(parameters[0]); - } - catch - { - OutputProc("Invalid port number!"); - return ExitCode.BadCommand; - } - var res = Server.GetComponent().SetRemoteAccessPort(port); - OutputProc(res ?? "Success!"); - return ExitCode.Normal; - } - } - - class AdminViewPortCommand : Command { - public AdminViewPortCommand() - { - Keyword = "view-port"; - } - public override string GetHelpText() - { - return "Print the port currently designated for remote access"; - } - - protected override ExitCode Run(IList parameters) - { - var port = Server.GetComponent().RemoteAccessPort(); - OutputProc(String.Format("{0}", port)); - return ExitCode.Normal; - } - } - - class AdminViewGroupCommand : Command + class AdminViewGroupCommand : ConsoleCommand { public AdminViewGroupCommand() { @@ -128,13 +50,13 @@ 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; } } - class AdminSetGroupCommand : Command + class AdminSetGroupCommand : ConsoleCommand { public AdminSetGroupCommand() { @@ -153,7 +75,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); @@ -167,7 +89,7 @@ namespace TGCommandLine } } - class AdminClearGroupCommand : Command + class AdminClearGroupCommand : ConsoleCommand { public AdminClearGroupCommand() { @@ -181,7 +103,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..2666104e44 100644 --- a/TGCommandLine/BYONDCommands.cs +++ b/TGCommandLine/BYONDCommands.cs @@ -2,10 +2,11 @@ using System.Collections.Generic; using System.Threading; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { - class BYONDCommand : RootCommand + class BYONDCommand : InstanceRootCommand { public BYONDCommand() { @@ -18,7 +19,7 @@ namespace TGCommandLine } } - class BYONDVersionCommand : Command + class BYONDVersionCommand : ConsoleCommand { public BYONDVersionCommand() { @@ -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() @@ -47,7 +48,7 @@ namespace TGCommandLine } - class BYONDStatusCommand : Command + class BYONDStatusCommand : ConsoleCommand { public BYONDStatusCommand() { @@ -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: @@ -87,7 +88,7 @@ namespace TGCommandLine } } - class BYONDUpdateCommand : Command + class BYONDUpdateCommand : ConsoleCommand { public BYONDUpdateCommand() { @@ -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..314a8725a1 100644 --- a/TGCommandLine/ChatCommands.cs +++ b/TGCommandLine/ChatCommands.cs @@ -1,34 +1,35 @@ using System; using System.Collections.Generic; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { - class IRCCommand : RootCommand + class IRCCommand : InstanceRootCommand { 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() { return "Manages the IRC bot"; } } - class DiscordCommand : RootCommand + class DiscordCommand : InstanceRootCommand { 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() { return "Manages the Discord bot"; } } - class IRCNickCommand : Command + class IRCNickCommand : ConsoleCommand { public IRCNickCommand() { @@ -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], }); @@ -56,10 +57,10 @@ namespace TGCommandLine } } - class ChatJoinCommand : Command + class ChatJoinCommand : ConsoleCommand { 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; @@ -135,10 +136,10 @@ namespace TGCommandLine } } - class ChatPartCommand : Command + class ChatPartCommand : ConsoleCommand { 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()) @@ -205,10 +206,10 @@ namespace TGCommandLine return ExitCode.Normal; } } - class ChatListAdminsCommand : Command + class ChatListAdminsCommand : ConsoleCommand { 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("+"); @@ -264,10 +265,10 @@ namespace TGCommandLine return ExitCode.Normal; } } - class ChatReconnectCommand : Command + class ChatReconnectCommand : ConsoleCommand { - 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); @@ -289,10 +290,10 @@ namespace TGCommandLine return ExitCode.Normal; } } - class ChatAddminCommand : Command + class ChatAddminCommand : ConsoleCommand { 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; @@ -336,7 +337,7 @@ namespace TGCommandLine return ExitCode.Normal; } } - class IRCAuthModeCommand : Command + class IRCAuthModeCommand : ConsoleCommand { public IRCAuthModeCommand() { @@ -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; @@ -375,7 +376,7 @@ namespace TGCommandLine return ExitCode.Normal; } } - class DiscordAuthModeCommand : Command + class DiscordAuthModeCommand : ConsoleCommand { public DiscordAuthModeCommand() { @@ -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; @@ -414,7 +415,7 @@ namespace TGCommandLine return ExitCode.Normal; } } - class IRCAuthLevelCommand : Command + class IRCAuthLevelCommand : ConsoleCommand { public IRCAuthLevelCommand() { @@ -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 "+": @@ -462,10 +463,10 @@ namespace TGCommandLine return ExitCode.Normal; } } - class ChatDeadminCommand : Command + class ChatDeadminCommand : ConsoleCommand { 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; @@ -510,7 +511,7 @@ namespace TGCommandLine } } - class IRCAuthCommand : Command + class IRCAuthCommand : ConsoleCommand { public IRCAuthCommand() { @@ -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] @@ -538,7 +539,7 @@ namespace TGCommandLine } } - class IRCDisableAuthCommand : Command + class IRCDisableAuthCommand : ConsoleCommand { public IRCDisableAuthCommand() { @@ -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, @@ -560,10 +561,10 @@ namespace TGCommandLine } } - class ChatStatusCommand : Command + class ChatStatusCommand : ConsoleCommand { 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:"); @@ -593,10 +594,10 @@ namespace TGCommandLine return ExitCode.Normal; } } - class ChatEnableCommand : Command + class ChatEnableCommand : ConsoleCommand { 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); @@ -621,10 +622,10 @@ namespace TGCommandLine return ExitCode.Normal; } } - class ChatDisableCommand : Command + class ChatDisableCommand : ConsoleCommand { 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); @@ -650,7 +651,7 @@ namespace TGCommandLine } } - class IRCServerCommand : Command + class IRCServerCommand : ConsoleCommand { public IRCServerCommand() { @@ -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] }; @@ -695,7 +696,7 @@ namespace TGCommandLine } } - class DiscordSetTokenCommand : Command + class DiscordSetTokenCommand : ConsoleCommand { public DiscordSetTokenCommand() { @@ -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..0aae903ed4 100644 --- a/TGCommandLine/ConfigCommands.cs +++ b/TGCommandLine/ConfigCommands.cs @@ -2,10 +2,11 @@ using System.Collections.Generic; using System.IO; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { - class ConfigCommand : RootCommand + class ConfigCommand : InstanceRootCommand { public ConfigCommand() { @@ -17,7 +18,7 @@ namespace TGCommandLine return "Manage settings"; } } - class ConfigDeleteCommand : Command + class ConfigDeleteCommand : ConsoleCommand { public ConfigDeleteCommand() { @@ -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); @@ -45,7 +46,7 @@ namespace TGCommandLine } } - class ConfigListCommand : Command + class ConfigListCommand : ConsoleCommand { public ConfigListCommand() { @@ -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); @@ -78,7 +79,7 @@ namespace TGCommandLine } } - class ConfigServerDirectoryCommand : Command + class ConfigServerDirectoryCommand : ConsoleCommand { public ConfigServerDirectoryCommand() { @@ -87,7 +88,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - OutputProc(Server.GetComponent().ServerDirectory()); + OutputProc(Interface.GetComponent().ServerDirectory()); return ExitCode.Normal; } @@ -97,7 +98,7 @@ namespace TGCommandLine } } - class ConfigDownloadCommand : Command + class ConfigDownloadCommand : ConsoleCommand { public ConfigDownloadCommand() { @@ -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); @@ -135,7 +136,7 @@ namespace TGCommandLine } } - class ConfigUploadCommand : Command + class ConfigUploadCommand : ConsoleCommand { public ConfigUploadCommand() { @@ -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/ConsoleCommand.cs b/TGCommandLine/ConsoleCommand.cs new file mode 100644 index 0000000000..8584017716 --- /dev/null +++ b/TGCommandLine/ConsoleCommand.cs @@ -0,0 +1,12 @@ +using TGServiceInterface; + +namespace TGCommandLine +{ + abstract class ConsoleCommand : Command + { + /// + /// The currently in use by the + /// + public static Interface Interface; + } +} diff --git a/TGCommandLine/DDCommands.cs b/TGCommandLine/DDCommands.cs index 8d263f12f5..ef518c8911 100644 --- a/TGCommandLine/DDCommands.cs +++ b/TGCommandLine/DDCommands.cs @@ -1,10 +1,11 @@ using System; using System.Collections.Generic; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { - class DDCommand : RootCommand + class DDCommand : InstanceRootCommand { public DDCommand() { @@ -17,7 +18,7 @@ namespace TGCommandLine } } - class DDWorldAnnounceCommand : Command + class DDWorldAnnounceCommand : ConsoleCommand { public DDWorldAnnounceCommand() { @@ -37,13 +38,13 @@ 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; } } - class DDStartCommand : Command + class DDStartCommand : ConsoleCommand { public DDStartCommand() { @@ -57,13 +58,13 @@ 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; } } - class DDStopCommand : Command + class DDStopCommand : ConsoleCommand { public DDStopCommand() { @@ -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; @@ -97,7 +98,7 @@ namespace TGCommandLine return res == null ? ExitCode.Normal : ExitCode.ServerError; } } - class DDRestartCommand : Command + class DDRestartCommand : ConsoleCommand { public DDRestartCommand() { @@ -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; @@ -131,7 +132,7 @@ namespace TGCommandLine return "Restarts the server and watchdog optionally waiting for the current round to end"; } } - class DDStatusCommand : Command + class DDStatusCommand : ConsoleCommand { public DDStatusCommand() { @@ -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."); @@ -156,7 +157,7 @@ namespace TGCommandLine } } - class DDAutostartCommand : Command + class DDAutostartCommand : ConsoleCommand { public DDAutostartCommand() { @@ -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": @@ -194,7 +195,7 @@ namespace TGCommandLine return "Change or check autostarting of the game server with the service"; } } - class DDWebclientCommand : Command + class DDWebclientCommand : ConsoleCommand { public DDWebclientCommand() { @@ -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": @@ -233,7 +234,7 @@ namespace TGCommandLine } } - class DDPortCommand : Command + class DDPortCommand : ConsoleCommand { public DDPortCommand() { @@ -254,7 +255,7 @@ namespace TGCommandLine return ExitCode.BadCommand; } - Server.GetComponent().SetPort(port); + Interface.GetComponent().SetPort(port); return ExitCode.Normal; } @@ -269,7 +270,7 @@ namespace TGCommandLine } } - class DDSecurityCommand : Command + class DDSecurityCommand : ConsoleCommand { public DDSecurityCommand() { @@ -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..cbc1ef6ae2 100644 --- a/TGCommandLine/DMCommands.cs +++ b/TGCommandLine/DMCommands.cs @@ -2,10 +2,11 @@ using System.Collections.Generic; using System.Threading; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { - class DMCommand : RootCommand + class DMCommand : InstanceRootCommand { public DMCommand() { @@ -18,7 +19,7 @@ namespace TGCommandLine } } - class DMCompileCommand : Command + class DMCompileCommand : ConsoleCommand { public DMCompileCommand() { @@ -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) @@ -74,7 +75,7 @@ namespace TGCommandLine return "Starts a compile/update job optionally waiting for completion"; } } - class DMStatusCommand : Command + class DMStatusCommand : ConsoleCommand { public DMStatusCommand() { @@ -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; @@ -122,7 +123,7 @@ namespace TGCommandLine } } - class DMSetProjectNameCommand : Command + class DMSetProjectNameCommand : ConsoleCommand { public DMSetProjectNameCommand() { @@ -141,12 +142,12 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - Server.GetComponent().SetProjectName(parameters[0]); + Interface.GetComponent().SetProjectName(parameters[0]); return ExitCode.Normal; } } - class DMInitializeCommand : Command + class DMInitializeCommand : ConsoleCommand { public DMInitializeCommand() { @@ -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) @@ -196,7 +197,7 @@ namespace TGCommandLine } } - class DMCancelCommand : Command + class DMCancelCommand : ConsoleCommand { public DMCancelCommand() { @@ -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/InstanceRootCommand.cs b/TGCommandLine/InstanceRootCommand.cs new file mode 100644 index 0000000000..34685e8395 --- /dev/null +++ b/TGCommandLine/InstanceRootCommand.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using TGServiceInterface; + +namespace TGCommandLine +{ + abstract class InstanceRootCommand : RootCommand + { + public static Interface currentInterface; + public override ExitCode DoRun(IList parameters) + { + if (currentInterface.InstanceName == null) + { + OutputProc("Missing instance!"); + return ExitCode.BadCommand; + } + else + { + var res = currentInterface.ConnectToInstance(); + if (!res.HasFlag(ConnectivityLevel.Connected)) + { + OutputProc("Unable to connect to instance!"); + return ExitCode.ConnectionError; + } + else if (!res.HasFlag(ConnectivityLevel.Authenticated)) + { + OutputProc("The current user is not authorized to use this instance!"); + return ExitCode.ConnectionError; + } + } + return base.DoRun(parameters); + } + } +} diff --git a/TGCommandLine/Program.cs b/TGCommandLine/Program.cs index 301da45f6c..1772582cc5 100644 --- a/TGCommandLine/Program.cs +++ b/TGCommandLine/Program.cs @@ -2,13 +2,16 @@ using System.Collections.Generic; using System.Linq; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { class Program { - static ExitCode RunCommandLine(IList argsAsList) + static bool interactive = false, saidSrvVersion = false; + static Interface currentInterface; + static Command.ExitCode RunCommandLine(IList argsAsList) { //first lookup the connection string bool badConnectionString = false; @@ -50,7 +53,7 @@ namespace TGCommandLine } argsAsList.RemoveAt(I); argsAsList.RemoveAt(I); - Server.SetRemoteLoginInformation(address, port, username, password); + ReplaceInterface(new Interface(address, port, username, password)); break; } } @@ -58,39 +61,53 @@ 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(); - if (res != null) + var res = currentInterface.ConnectionStatus(out string error); + if (!res.HasFlag(ConnectivityLevel.Connected)) { - Console.WriteLine("Unable to connect to service: " + res); + Console.WriteLine("Unable to connect to service: " + error); Console.WriteLine("Remote connection usage: <-c/--connect> username:password@address:port"); - return ExitCode.ConnectionError; + return Command.ExitCode.ConnectionError; } - if (!Server.Authenticate()) + if (!res.HasFlag(ConnectivityLevel.Authenticated)) { 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 && currentInterface.VersionMismatch(out error)) { SentVMMWarning = true; Console.WriteLine(error); } + else if (interactive && !saidSrvVersion) + { + Console.WriteLine("Connectd to service version: " + currentInterface.GetService().Version()); + saidSrvVersion = true; + } try { - return new CLICommand().DoRun(argsAsList); + return new CLICommand(currentInterface).DoRun(argsAsList); } catch (Exception e) { Console.WriteLine("Error: " + e.ToString()); - return ExitCode.ConnectionError; + return Command.ExitCode.ConnectionError; }; } + + static void ReplaceInterface(Interface I) + { + currentInterface = I; + ConsoleCommand.Interface = I; + InstanceRootCommand.currentInterface = I; + saidSrvVersion = false; + } + public static string ReadLineSecure() { string result = ""; @@ -135,31 +152,67 @@ namespace TGCommandLine return false; } + /// + /// Tries to set 's to , outputting appropriate messages + /// + /// The name of the to test + /// If , does not output on success + /// if a was achieved with , otherwise + static bool CheckInstanceConnectivity(string instanceName, bool silentSuccess) + { + var res = currentInterface.ConnectToInstance(instanceName); + if (!res.HasFlag(ConnectivityLevel.Connected)) + Console.WriteLine("Unable to connect to instance! Does it exist?"); + else if (!res.HasFlag(ConnectivityLevel.Authenticated)) + Console.WriteLine("The current user is not authorized to use this instance!"); + else + { + if(!silentSuccess) + Console.WriteLine("Successfully conected to instance!"); + return true; + } + return false; + } + static int Main(string[] args) { + ReplaceInterface(new Interface()); Command.OutputProcVar.Value = Console.WriteLine; if (args.Length != 0) { - Server.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; - return false; - }); - //allow self signed certs in debug mode - return (int)RunCommandLine(new List(args)); + var argsAsList = new List(args); + for (var I = 0; I < argsAsList.Count - 1; ++I) + { + if (argsAsList[I].ToLower() == "--instance") + { + if (!CheckInstanceConnectivity(args[I + 1], true)) + return (int)Command.ExitCode.ConnectionError; + argsAsList.RemoveRange(I, 2); + break; + } + else if (argsAsList[I].ToLower() == "--disable-ssl-verification") //im just not even going to document this because i hate it so much + { + argsAsList.RemoveAt(I); + --I; + Interface.SetBadCertificateHandler(_ => false); + } + } + return (int)RunCommandLine(argsAsList); } - - Server.SetBadCertificateHandler(BadCertificateInteractive); - - Console.WriteLine("Type 'remote' to connect to a remote service"); //interactive mode + Interface.SetBadCertificateHandler(BadCertificateInteractive); + Console.WriteLine("Type 'instance' to connect to a server instance"); + Console.WriteLine("Type 'remote' to connect to a remote service"); while (true) { Console.Write("Enter command: "); var NextCommand = Console.ReadLine(); switch (NextCommand.ToLower()) { + case "instance": + Console.Write("Enter instance name: "); + CheckInstanceConnectivity(Console.ReadLine(), false); + break; case "remote": SentVMMWarning = false; Console.Write("Enter server address: "); @@ -178,41 +231,41 @@ namespace TGCommandLine var username = Console.ReadLine(); Console.Write("Enter password: "); var password = ReadLineSecure(); - Server.SetRemoteLoginInformation(address, port, username, password); - var res = Server.VerifyConnection(); - if (res != null) + ReplaceInterface(new Interface(address, port, username, password)); + var res = currentInterface.ConnectionStatus(out string error); + if (!res.HasFlag(ConnectivityLevel.Connected)) { - Console.WriteLine("Unable to connect: " + res); - Server.MakeLocalConnection(); + Console.WriteLine("Unable to connect: " + error); + ReplaceInterface(new Interface()); } - else if (!Server.Authenticate()) + else if (!res.HasFlag(ConnectivityLevel.Authenticated)) { Console.WriteLine("Authentication error: Username/password/windows identity is not authorized! Returning to local mode..."); - Server.MakeLocalConnection(); + ReplaceInterface(new Interface()); } else { Console.WriteLine("Connected remotely"); - if (Server.VersionMismatch(out res)) + if (currentInterface.VersionMismatch(out error)) { SentVMMWarning = true; - Console.WriteLine(res); + Console.WriteLine(error); } Console.WriteLine("Type 'disconnect' to return to local mode"); } break; case "disconnect": SentVMMWarning = false; - Server.MakeLocalConnection(); + ReplaceInterface(new Interface()); 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; + currentInterface.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 c76e6431c5..9e3fe3e658 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 { @@ -9,7 +10,7 @@ namespace TGCommandLine public RepoCommand() { Keyword = "repo"; - Children = new Command[] { new RepoSetupCommand(), new RepoUpdateCommand(), new RepoGenChangelogCommand(), new RepoPushChangelogCommand(), new RepoPythonPathCommand(), new RepoSetEmailCommand(), new RepoSetNameCommand(), new RepoMergePRCommand(), new RepoListPRsCommand(), new RepoStatusCommand(), new RepoListBackupsCommand(), new RepoCheckoutCommand(), new RepoResetCommand(), new RepoUpdateJsonCommand(), new RepoSetPushTestmergeCommitsCommand() }; + Children = new Command[] { new RepoSetupCommand(), new RepoUpdateCommand(), new RepoGenChangelogCommand(), new RepoPushChangelogCommand(), new RepoSetEmailCommand(), new RepoSetNameCommand(), new RepoMergePRCommand(), new RepoListPRsCommand(), new RepoStatusCommand(), new RepoListBackupsCommand(), new RepoCheckoutCommand(), new RepoResetCommand(), new RepoUpdateJsonCommand(), new RepoSetPushTestmergeCommitsCommand() }; } public override string GetHelpText() { @@ -17,7 +18,7 @@ namespace TGCommandLine } } - class RepoSetPushTestmergeCommitsCommand : Command + class RepoSetPushTestmergeCommitsCommand : ConsoleCommand { public RepoSetPushTestmergeCommitsCommand() { @@ -39,10 +40,10 @@ namespace TGCommandLine switch (parameters[0].ToLower()) { case "on": - Server.GetComponent().SetPushTestmergeCommits(true); + Interface.GetComponent().SetPushTestmergeCommits(true); break; case "off": - Server.GetComponent().SetPushTestmergeCommits(false); + Interface.GetComponent().SetPushTestmergeCommits(false); break; default: OutputProc("Invalid option!"); @@ -52,7 +53,7 @@ namespace TGCommandLine } } - class RepoUpdateJsonCommand : Command + class RepoUpdateJsonCommand : ConsoleCommand { public RepoUpdateJsonCommand() { @@ -66,7 +67,7 @@ namespace TGCommandLine protected override ExitCode Run(IList parameters) { - var res = Server.GetComponent().UpdateTGS3Json(); + var res = Interface.GetComponent().UpdateTGS3Json(); if (res != null) { OutputProc(res); @@ -76,7 +77,7 @@ namespace TGCommandLine } } - class RepoSetupCommand : Command + class RepoSetupCommand : ConsoleCommand { public RepoSetupCommand() { @@ -85,7 +86,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); @@ -104,7 +105,7 @@ namespace TGCommandLine } } - class RepoStatusCommand : Command + class RepoStatusCommand : ConsoleCommand { public RepoStatusCommand() { @@ -112,7 +113,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - var Repo = Server.GetComponent(); + var Repo = Interface.GetComponent(); var busy = Repo.OperationInProgress(); if (!busy) { @@ -158,7 +159,7 @@ namespace TGCommandLine } } - class RepoResetCommand : Command + class RepoResetCommand : ConsoleCommand { public RepoResetCommand() { @@ -166,7 +167,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; } @@ -180,7 +181,7 @@ namespace TGCommandLine } } - class RepoUpdateCommand : Command + class RepoUpdateCommand : ConsoleCommand { public RepoUpdateCommand() { @@ -202,7 +203,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; } @@ -215,7 +216,7 @@ namespace TGCommandLine return ""; } } - class RepoGenChangelogCommand : Command + class RepoGenChangelogCommand : ConsoleCommand { public RepoGenChangelogCommand() { @@ -223,7 +224,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); @@ -235,7 +236,7 @@ namespace TGCommandLine return "Compiles the html changelog"; } } - class RepoPushChangelogCommand : Command + class RepoPushChangelogCommand : ConsoleCommand { public RepoPushChangelogCommand() { @@ -243,7 +244,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; @@ -254,7 +255,7 @@ namespace TGCommandLine return "Pushes the html changelog if the SSH authentication is configured correctly"; } } - class RepoSetEmailCommand : Command + class RepoSetEmailCommand : ConsoleCommand { public RepoSetEmailCommand() { @@ -263,7 +264,7 @@ namespace TGCommandLine } protected override ExitCode Run(IList parameters) { - Server.GetComponent().SetCommitterEmail(parameters[0]); + Interface.GetComponent().SetCommitterEmail(parameters[0]); return ExitCode.Normal; } @@ -276,7 +277,7 @@ namespace TGCommandLine return "Set the e-mail used for commits"; } } - class RepoSetNameCommand : Command + class RepoSetNameCommand : ConsoleCommand { public RepoSetNameCommand() { @@ -285,7 +286,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() @@ -297,29 +298,8 @@ namespace TGCommandLine return "Set the name used for commits"; } } - class RepoPythonPathCommand : Command - { - public RepoPythonPathCommand() - { - Keyword = "python-path"; - RequiredParameters = 1; - } - protected override ExitCode Run(IList parameters) - { - Server.GetComponent().SetPythonPath(parameters[0]); - return ExitCode.Normal; - } - public override string GetArgumentString() - { - return ""; - } - public override string GetHelpText() - { - return "Set the path to the folder containing the python 2.7 installation"; - } - } - class RepoMergePRCommand : Command + class RepoMergePRCommand : ConsoleCommand { public RepoMergePRCommand() { @@ -339,7 +319,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; } @@ -354,7 +334,7 @@ namespace TGCommandLine } } - class RepoListPRsCommand : Command + class RepoListPRsCommand : ConsoleCommand { public RepoListPRsCommand() { @@ -366,7 +346,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); @@ -381,7 +361,7 @@ namespace TGCommandLine } } - class RepoListBackupsCommand : Command + class RepoListBackupsCommand : ConsoleCommand { public RepoListBackupsCommand() { @@ -393,7 +373,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); @@ -407,7 +387,7 @@ namespace TGCommandLine return ExitCode.Normal; } } - class RepoCheckoutCommand : Command + class RepoCheckoutCommand : ConsoleCommand { public RepoCheckoutCommand() { @@ -420,7 +400,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..4e6e3d4b69 100644 --- a/TGCommandLine/RootCommands.cs +++ b/TGCommandLine/RootCommands.cs @@ -1,16 +1,19 @@ using System; using System.Collections.Generic; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGCommandLine { class CLICommand : RootCommand { - public CLICommand() + public CLICommand(Interface I) { 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 (I.ConnectToInstance().HasFlag(ConnectivityLevel.Administrator)) tmp.Add(new AdminCommand()); + if (I.ConnectionStatus().HasFlag(ConnectivityLevel.Administrator)) + tmp.Add(new ServiceCommand()); Children = tmp.ToArray(); } @@ -20,7 +23,7 @@ namespace TGCommandLine base.PrintHelp(); } } - class AutoUpdateCommand : Command + class AutoUpdateCommand : ConsoleCommand { public AutoUpdateCommand() { @@ -34,12 +37,12 @@ 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; } } - class SetAutoUpdateCommand : Command + class SetAutoUpdateCommand : ConsoleCommand { public SetAutoUpdateCommand() { @@ -73,12 +76,12 @@ namespace TGCommandLine return ExitCode.BadCommand; } - Server.GetComponent().SetAutoUpdateInterval(NewInterval); + Interface.GetComponent().SetAutoUpdateInterval(NewInterval); return ExitCode.Normal; } } - class UpdateCommand : Command + class UpdateCommand : ConsoleCommand { public UpdateCommand() { @@ -88,7 +91,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 +121,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; } @@ -139,7 +142,7 @@ namespace TGCommandLine } } - class TestmergeCommand : Command + class TestmergeCommand : ConsoleCommand { public TestmergeCommand() { @@ -160,7 +163,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 +176,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/ServiceCommands.cs b/TGCommandLine/ServiceCommands.cs new file mode 100644 index 0000000000..efbc628e7d --- /dev/null +++ b/TGCommandLine/ServiceCommands.cs @@ -0,0 +1,433 @@ +using System; +using System.Collections.Generic; +using TGServiceInterface; + +namespace TGCommandLine +{ + /// + /// Used for managing the + /// + class ServiceCommand : RootCommand + { + /// + /// Construct a + /// + public ServiceCommand() + { + Keyword = "service"; + Children = new Command[] { new ServiceCreateInstanceCommand(), new ServiceDetachInstanceCommand(), new ServiceEnableInstanceCommand(), new ServiceImportInstanceCommand(), new ServiceListInstancesCommand(), new ServicePythonPathCommand(), new ServiceSetPythonPathCommand(), new ServiceSetRemoteAccessPortCommand(), new ServiceRemoteAccessPortCommand(), new ServiceDisableInstanceCommand(), new ServiceRenameInstanceCommand() }; + } + + /// + public override string GetHelpText() + { + return "Manage service wide settings"; + } + } + + /// + /// Command for calling + /// + class ServiceCreateInstanceCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceCreateInstanceCommand() + { + Keyword = "create-instance"; + RequiredParameters = 2; + } + + /// + public override string GetHelpText() + { + return "Creates a new instance at the given path"; + } + + /// + public override string GetArgumentString() + { + return " "; + } + + /// + protected override ExitCode Run(IList parameters) + { + var res = Interface.GetService().CreateInstance(parameters[0], parameters[1]); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServiceListInstancesCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceListInstancesCommand() + { + Keyword = "list-instances"; + } + + /// + public override string GetHelpText() + { + return "Lists all instances"; + } + + /// + protected override ExitCode Run(IList parameters) + { + foreach (var I in Interface.GetService().ListInstances()) + OutputProc(String.Format("{0} ({1}):\t{2}{3}", I.Name, I.Path, I.Enabled ? "Online" : "Offline", I.Enabled ? String.Format(" ({0})", I.LoggingID) : "")); + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServiceDetachInstanceCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceDetachInstanceCommand() + { + Keyword = "detach-instance"; + RequiredParameters = 1; + } + + /// + public override string GetHelpText() + { + return "Detaches an instance"; + } + + /// + public override string GetArgumentString() + { + return ""; + } + + /// + protected override ExitCode Run(IList parameters) + { + var res = Interface.GetService().DetachInstance(parameters[0]); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServiceImportInstanceCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceImportInstanceCommand() + { + Keyword = "import-instance"; + RequiredParameters = 1; + } + + /// + public override string GetHelpText() + { + return "Imports an instance (Chat settings will be lost if they are from a different windows installation)"; + } + + /// + public override string GetArgumentString() + { + return ""; + } + + /// + protected override ExitCode Run(IList parameters) + { + var res = Interface.GetService().ImportInstance(parameters[0]); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServicePythonPathCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServicePythonPathCommand() + { + Keyword = "python"; + } + + /// + public override string GetHelpText() + { + return "Displays configured path the service uses for python"; + } + + /// + protected override ExitCode Run(IList parameters) + { + var res = Interface.GetService().PythonPath(); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServiceSetPythonPathCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceSetPythonPathCommand() + { + Keyword = "set-python"; + RequiredParameters = 1; + } + + /// + public override string GetHelpText() + { + return "Sets the path to the python installation"; + } + + /// + public override string GetArgumentString() + { + return ""; + } + + /// + protected override ExitCode Run(IList parameters) + { + Interface.GetService().SetPythonPath(parameters[0]); + return ExitCode.Normal; + } + } + + /// + /// Command for calling with a parameter + /// + class ServiceEnableInstanceCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceEnableInstanceCommand() + { + Keyword = "enable-instance"; + RequiredParameters = 1; + } + + /// + public override string GetHelpText() + { + return "Enables the specified instance"; + } + + /// + public override string GetArgumentString() + { + return ""; + } + + /// + protected override ExitCode Run(IList parameters) + { + var res = Interface.GetService().SetInstanceEnabled(parameters[0], true); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + return ExitCode.Normal; + } + } + + /// + /// Command for calling with a parameter + /// + class ServiceDisableInstanceCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceDisableInstanceCommand() + { + Keyword = "disable-instance"; + RequiredParameters = 1; + } + + /// + public override string GetHelpText() + { + return "Disables the specified instance"; + } + + /// + public override string GetArgumentString() + { + return ""; + } + + /// + protected override ExitCode Run(IList parameters) + { + var res = Interface.GetService().SetInstanceEnabled(parameters[0], false); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServiceRemoteAccessPortCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceRemoteAccessPortCommand() + { + Keyword = "port"; + } + + /// + public override string GetHelpText() + { + return "Displays the service's remote access port"; + } + + /// + protected override ExitCode Run(IList parameters) + { + OutputProc(Interface.GetService().RemoteAccessPort().ToString()); + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServiceSetRemoteAccessPortCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceSetRemoteAccessPortCommand() + { + Keyword = "set-port"; + RequiredParameters = 1; + } + + /// + public override string GetHelpText() + { + return "Sets the service's remote access port"; + } + + /// + public override string GetArgumentString() + { + return ""; + } + + /// + protected override ExitCode Run(IList parameters) + { + ushort port; + try + { + port = Convert.ToUInt16(parameters[0]); + } + catch + { + OutputProc("Invalid port number!"); + return ExitCode.BadCommand; + } + + var res = Interface.GetService().SetRemoteAccessPort(port); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + OutputProc("Change will be applied after service restart"); + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServiceRenameInstanceCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceRenameInstanceCommand() + { + Keyword = "rename-instance"; + RequiredParameters = 1; + } + + /// + public override string GetHelpText() + { + return "Renames an instance. Will temporarily disable the instance if it is active"; + } + + /// + public override string GetArgumentString() + { + return " "; + } + + /// + protected override ExitCode Run(IList parameters) + { + var res = Interface.GetService().RenameInstance(parameters[0], parameters[1]); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + return ExitCode.Normal; + } + } +} diff --git a/TGCommandLine/TGCommandLine.csproj b/TGCommandLine/TGCommandLine.csproj index cf32e3fcf0..0c299ad037 100644 --- a/TGCommandLine/TGCommandLine.csproj +++ b/TGCommandLine/TGCommandLine.csproj @@ -12,44 +12,49 @@ 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\Debug\ + DEBUG;TRACE + full + AnyCPU + prompt + MinimumRecommendedRules.ruleset + true + + + bin\Release\ + TRACE + bin\x86\Release\TGCommandLine.xml + true + true + pdbonly + AnyCPU + prompt + MinimumRecommendedRules.ruleset + true + + + + diff --git a/TGControlPanel/ByondPage.cs b/TGControlPanel/ControlPanel/ByondPage.cs similarity index 68% rename from TGControlPanel/ByondPage.cs rename to TGControlPanel/ControlPanel/ByondPage.cs index 62fccce395..df284edc12 100644 --- a/TGControlPanel/ByondPage.cs +++ b/TGControlPanel/ControlPanel/ByondPage.cs @@ -1,18 +1,19 @@ using System; using System.Windows.Forms; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGControlPanel { - partial class Main + partial class ControlPanel { 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/ControlPanel/ChatPage.cs similarity index 87% rename from TGControlPanel/ChatPage.cs rename to TGControlPanel/ControlPanel/ChatPage.cs index 74eee5244d..bea75efbff 100644 --- a/TGControlPanel/ChatPage.cs +++ b/TGControlPanel/ControlPanel/ChatPage.cs @@ -2,30 +2,31 @@ using System.Collections.Generic; using System.Windows.Forms; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGControlPanel { - partial class Main + partial class ControlPanel { 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/Main.Designer.cs b/TGControlPanel/ControlPanel/ControlPanel.Designer.cs similarity index 98% rename from TGControlPanel/Main.Designer.cs rename to TGControlPanel/ControlPanel/ControlPanel.Designer.cs index 4ce2b177a5..766093f7f5 100644 --- a/TGControlPanel/Main.Designer.cs +++ b/TGControlPanel/ControlPanel/ControlPanel.Designer.cs @@ -1,6 +1,6 @@ namespace TGControlPanel { - partial class Main + partial class ControlPanel { /// /// Required designer variable. @@ -15,6 +15,7 @@ { if (disposing && (components != null)) { + Cleanup(); components.Dispose(); } base.Dispose(disposing); @@ -28,7 +29,7 @@ /// private void InitializeComponent() { - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Main)); + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ControlPanel)); this.RepoBGW = new System.ComponentModel.BackgroundWorker(); this.FullUpdateWorker = new System.ComponentModel.BackgroundWorker(); this.ServerStartBGW = new System.ComponentModel.BackgroundWorker(); @@ -78,7 +79,6 @@ this.WebclientCheckBox = new System.Windows.Forms.CheckBox(); this.WorldAnnounceButton = new System.Windows.Forms.Button(); this.WorldAnnounceField = new System.Windows.Forms.TextBox(); - this.ServerPathTextbox = new System.Windows.Forms.TextBox(); this.projectNameText = new System.Windows.Forms.TextBox(); this.WorldAnnounceLabel = new System.Windows.Forms.Label(); this.SecuritySelector = new System.Windows.Forms.ComboBox(); @@ -126,10 +126,8 @@ this.BackupTagsList = new System.Windows.Forms.ListBox(); this.ResetRemote = new System.Windows.Forms.Button(); this.RecloneButton = new System.Windows.Forms.Button(); - this.PythonPathText = new System.Windows.Forms.TextBox(); this.RepoBranchTextBox = new System.Windows.Forms.TextBox(); this.RepoRemoteTextBox = new System.Windows.Forms.TextBox(); - this.PythonPathLabel = new System.Windows.Forms.Label(); this.RepoGenChangelogButton = new System.Windows.Forms.Button(); this.TestmergeSelector = new System.Windows.Forms.NumericUpDown(); this.TestMergeListLabel = new System.Windows.Forms.ListBox(); @@ -644,7 +642,6 @@ this.ServerPanel.Controls.Add(this.WebclientCheckBox); this.ServerPanel.Controls.Add(this.WorldAnnounceButton); this.ServerPanel.Controls.Add(this.WorldAnnounceField); - this.ServerPanel.Controls.Add(this.ServerPathTextbox); this.ServerPanel.Controls.Add(this.projectNameText); this.ServerPanel.Controls.Add(this.WorldAnnounceLabel); this.ServerPanel.Controls.Add(this.SecuritySelector); @@ -791,14 +788,6 @@ this.WorldAnnounceField.Size = new System.Drawing.Size(213, 20); this.WorldAnnounceField.TabIndex = 40; // - // ServerPathTextbox - // - this.ServerPathTextbox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.ServerPathTextbox.Location = new System.Drawing.Point(136, 163); - this.ServerPathTextbox.Name = "ServerPathTextbox"; - this.ServerPathTextbox.Size = new System.Drawing.Size(296, 20); - this.ServerPathTextbox.TabIndex = 32; - // // projectNameText // this.projectNameText.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); @@ -1326,10 +1315,8 @@ this.RepoPanel.Controls.Add(this.BackupTagsList); this.RepoPanel.Controls.Add(this.ResetRemote); this.RepoPanel.Controls.Add(this.RecloneButton); - this.RepoPanel.Controls.Add(this.PythonPathText); this.RepoPanel.Controls.Add(this.RepoBranchTextBox); this.RepoPanel.Controls.Add(this.RepoRemoteTextBox); - this.RepoPanel.Controls.Add(this.PythonPathLabel); this.RepoPanel.Controls.Add(this.RepoGenChangelogButton); this.RepoPanel.Controls.Add(this.TestmergeSelector); this.RepoPanel.Controls.Add(this.TestMergeListLabel); @@ -1413,14 +1400,6 @@ this.RecloneButton.Visible = false; this.RecloneButton.Click += new System.EventHandler(this.RecloneButton_Click); // - // PythonPathText - // - this.PythonPathText.Location = new System.Drawing.Point(122, 112); - this.PythonPathText.Name = "PythonPathText"; - this.PythonPathText.Size = new System.Drawing.Size(535, 20); - this.PythonPathText.TabIndex = 31; - this.PythonPathText.Visible = false; - // // RepoBranchTextBox // this.RepoBranchTextBox.Location = new System.Drawing.Point(122, 70); @@ -1437,19 +1416,6 @@ this.RepoRemoteTextBox.TabIndex = 14; this.RepoRemoteTextBox.Visible = false; // - // PythonPathLabel - // - this.PythonPathLabel.AutoSize = true; - this.PythonPathLabel.Font = new System.Drawing.Font("Verdana", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.PythonPathLabel.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(248)))), ((int)(((byte)(248)))), ((int)(((byte)(242))))); - this.PythonPathLabel.Location = new System.Drawing.Point(6, 112); - this.PythonPathLabel.Name = "PythonPathLabel"; - this.PythonPathLabel.Size = new System.Drawing.Size(114, 18); - this.PythonPathLabel.TabIndex = 30; - this.PythonPathLabel.Text = "Python Path:"; - this.PythonPathLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; - this.PythonPathLabel.Visible = false; - // // RepoGenChangelogButton // this.RepoGenChangelogButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); @@ -1898,8 +1864,6 @@ private System.Windows.Forms.CheckBox WebclientCheckBox; private System.Windows.Forms.Button WorldAnnounceButton; private System.Windows.Forms.TextBox WorldAnnounceField; - private System.Windows.Forms.TextBox ServerPathTextbox; - private System.Windows.Forms.TextBox projectNameText; private System.Windows.Forms.Label WorldAnnounceLabel; private System.Windows.Forms.ComboBox SecuritySelector; private System.Windows.Forms.Label SecurityTitle; @@ -1944,10 +1908,8 @@ private System.Windows.Forms.ListBox BackupTagsList; private System.Windows.Forms.Button ResetRemote; private System.Windows.Forms.Button RecloneButton; - private System.Windows.Forms.TextBox PythonPathText; private System.Windows.Forms.TextBox RepoBranchTextBox; private System.Windows.Forms.TextBox RepoRemoteTextBox; - private System.Windows.Forms.Label PythonPathLabel; private System.Windows.Forms.Button RepoGenChangelogButton; private System.Windows.Forms.NumericUpDown TestmergeSelector; private System.Windows.Forms.ListBox TestMergeListLabel; @@ -1982,6 +1944,7 @@ private System.Windows.Forms.NumericUpDown AutoUpdateInterval; private System.Windows.Forms.CheckBox AutoUpdateCheckbox; private System.Windows.Forms.Label AutoUpdateMLabel; + private System.Windows.Forms.TextBox projectNameText; private System.Windows.Forms.CheckBox SyncCommitsCheckBox; } } diff --git a/TGControlPanel/ControlPanel/ControlPanel.cs b/TGControlPanel/ControlPanel/ControlPanel.cs new file mode 100644 index 0000000000..d047906de6 --- /dev/null +++ b/TGControlPanel/ControlPanel/ControlPanel.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Windows.Forms; +using TGServiceInterface; +using TGServiceInterface.Components; + +namespace TGControlPanel +{ + /// + /// The main form + /// + partial class ControlPanel : CountedForm + { + /// + /// List of instances being used by open control panels + /// + public static IDictionary InstancesInUse { get; private set; } = new Dictionary(); + + /// + /// The instance for this + /// + readonly Interface Interface; + + /// + /// Constructs a + /// + /// The for the + public ControlPanel(Interface I) + { + InitializeComponent(); + Interface = I; + if (Interface.IsRemoteConnection) + { + var splits = Interface.GetComponent().Version().Split(' '); + Text = String.Format("TGS {0}: {1}:{2}", splits[splits.Length - 1], Interface.HTTPSURL, Interface.HTTPSPort); + } + Text += " Instance: " + I.InstanceName; + if (Interface.VersionMismatch(out string error) && MessageBox.Show(error, "Warning", MessageBoxButtons.OKCancel) == DialogResult.Cancel) + { + Close(); + return; + } + Panels.SelectedIndexChanged += Panels_SelectedIndexChanged; + Panels.SelectedIndex += Math.Min(Properties.Settings.Default.LastPageIndex, Panels.TabCount - 1); + InitRepoPage(); + InitBYONDPage(); + InitServerPage(); + LoadChatPage(); + InitStaticPage(); + InstancesInUse.Add(I.InstanceName, this); + } + + /// + /// Called from + /// + void Cleanup() + { + InstancesInUse.Remove(Interface.InstanceName); + Interface.Dispose(); + } + + private void Main_Resize(object sender, EventArgs e) + { + Panels.Location = new Point(10, 10); + Panels.Width = ClientSize.Width - 20; + Panels.Height = ClientSize.Height - 20; + } + + private void Panels_SelectedIndexChanged(object sender, EventArgs e) + { + switch (Panels.SelectedIndex) + { + case 0: //repo + PopulateRepoFields(); + break; + case 1: //byond + UpdateBYONDButtons(); + break; + case 2: //scp + LoadServerPage(); + break; + case 3: //chat + LoadChatPage(); + break; + } + Properties.Settings.Default.LastPageIndex = Panels.SelectedIndex; + } + + bool CheckAdminWithWarning() + { + if (!Interface.ConnectToInstance().HasFlag(ConnectivityLevel.Administrator)) + { + MessageBox.Show("Only system administrators may use this command!"); + return false; + } + return true; + } + } +} diff --git a/TGControlPanel/Main.resx b/TGControlPanel/ControlPanel/ControlPanel.resx similarity index 100% rename from TGControlPanel/Main.resx rename to TGControlPanel/ControlPanel/ControlPanel.resx diff --git a/TGControlPanel/RepoPage.cs b/TGControlPanel/ControlPanel/RepoPage.cs similarity index 92% rename from TGControlPanel/RepoPage.cs rename to TGControlPanel/ControlPanel/RepoPage.cs index 2546e1d93a..fd4d775e9a 100644 --- a/TGControlPanel/RepoPage.cs +++ b/TGControlPanel/ControlPanel/RepoPage.cs @@ -3,10 +3,11 @@ using System.ComponentModel; using System.Windows.Forms; using System.Threading; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGControlPanel { - partial class Main + partial class ControlPanel { enum RepoAction { Clone, @@ -76,7 +77,7 @@ namespace TGControlPanel if (RepoBusyCheck()) return; - var Repo = Server.GetComponent(); + var Repo = Interface.GetComponent(); RepoProgressBar.Style = ProgressBarStyle.Marquee; RepoProgressBar.Visible = false; @@ -84,11 +85,8 @@ namespace TGControlPanel RepoRemoteTextBox.Visible = true; BranchNameTitle.Visible = true; RepoBranchTextBox.Visible = true; - PythonPathLabel.Visible = true; - PythonPathText.Visible = true; RepoRefreshButton.Visible = true; SyncCommitsCheckBox.Visible = true; - PythonPathText.Text = Repo.PythonPath(); SyncCommitsCheckBox.Checked = Repo.PushTestmergeCommits(); if (!Repo.Exists()) @@ -151,7 +149,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; @@ -173,7 +171,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,11 +210,7 @@ namespace TGControlPanel RepoBGW.ReportProgress(Repo.CheckoutProgress()); } while (Repo.OperationInProgress()); } - void UpdatePythonPath() - { - if (!Server.GetComponent().SetPythonPath(PythonPathText.Text)) - MessageBox.Show("Python could not be found in the selected location!"); - } + private void CloneRepositoryButton_Click(object sender, EventArgs e) { CloneRepo(); @@ -226,7 +220,6 @@ namespace TGControlPanel { CloneRepoURL = RepoRemoteTextBox.Text; CheckoutBranch = RepoBranchTextBox.Text; - UpdatePythonPath(); DoAsyncOp(RepoAction.Clone, String.Format("Cloning {0} branch of {1}...", CheckoutBranch, CloneRepoURL)); } @@ -239,7 +232,7 @@ namespace TGControlPanel } void DoAsyncOp(RepoAction ra, string message) { - if (ra != RepoAction.Wait && RepoBusyCheck()) + if (RepoBGW.IsBusy || (ra != RepoAction.Wait && RepoBusyCheck())) return; SyncCommitsCheckBox.Visible = false; @@ -260,8 +253,6 @@ namespace TGControlPanel IdentityLabel.Visible = false; TestmergeSelector.Visible = false; RepoGenChangelogButton.Visible = false; - PythonPathLabel.Visible = false; - PythonPathText.Visible = false; RecloneButton.Visible = false; ResetRemote.Visible = false; BackupTagsList.Visible = false; @@ -283,7 +274,7 @@ namespace TGControlPanel } private void RepoApplyButton_Click(object sender, EventArgs e) { - var Repo = Server.GetComponent(); + var Repo = Interface.GetComponent(); if (RepoBusyCheck()) return; @@ -315,8 +306,6 @@ namespace TGControlPanel CheckoutBranch = RepoBranchTextBox.Text; if(branch != CheckoutBranch) DoAsyncOp(RepoAction.Checkout, String.Format("Checking out {0}...", CheckoutBranch)); - - UpdatePythonPath(); } else CloneRepositoryButton_Click(null, null); @@ -356,7 +345,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/ControlPanel/ServerPage.cs similarity index 75% rename from TGControlPanel/ServerPage.cs rename to TGControlPanel/ControlPanel/ServerPage.cs index 0f001d3abc..9491b8db2a 100644 --- a/TGControlPanel/ServerPage.cs +++ b/TGControlPanel/ControlPanel/ServerPage.cs @@ -1,506 +1,465 @@ -using System; -using System.ComponentModel; -using System.Windows.Forms; -using TGServiceInterface; - -namespace TGControlPanel -{ - partial class Main - { - enum FullUpdateAction - { - UpdateHard, - UpdateMerge, - UpdateHardTestmerge, - Reset, - Testmerge, - } - - FullUpdateAction fuAction; - ushort testmergePR; - string updateError; - bool updatingFields = false; - - void InitServerPage() - { - LoadServerPage(); - if (!Server.AuthenticateAdmin()) - { - ServerPathTextbox.Enabled = false; - ServerPathTextbox.ReadOnly = true; - } - FullUpdateWorker.RunWorkerCompleted += FullUpdateWorker_RunWorkerCompleted; - ServerPathTextbox.LostFocus += ServerPathTextbox_LostFocus; - ServerPathTextbox.KeyDown += ServerPathTextbox_KeyDown; - projectNameText.LostFocus += ProjectNameText_LostFocus; - projectNameText.KeyDown += ProjectNameText_KeyDown; - ServerStartBGW.RunWorkerCompleted += ServerStartBGW_RunWorkerCompleted; - } - - private void ServerStartBGW_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) - { - if (e.Result != null) - MessageBox.Show((string)e.Result); - LoadServerPage(); - } - - private void ProjectNameText_KeyDown(object sender, KeyEventArgs e) - { - if (e.KeyCode == Keys.Enter) - UpdateProjectName(); - } - - private void ServerPathTextbox_KeyDown(object sender, KeyEventArgs e) - { - if (e.KeyCode == Keys.Enter) - UpdateServerPath(); - } - - private void FullUpdateWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) - { - if (updateError != null) - MessageBox.Show(updateError); - UpdateHardButton.Enabled = true; - UpdateMergeButton.Enabled = true; - TestmergeButton.Enabled = true; - UpdateTestmergeButton.Enabled = true; - LoadServerPage(); - } - - private void CompileCancelButton_Click(object sender, EventArgs e) - { - var res = Server.GetComponent().Cancel(); - if (res != null) - MessageBox.Show(res); - LoadServerPage(); - } - - private void ServerPathTextbox_LostFocus(object sender, EventArgs e) - { - UpdateServerPath(); - } - - void UpdateServerPath() - { - if (!Program.CheckAdminWithWarning()) - { - ServerPathTextbox.Enabled = false; - ServerPathTextbox.ReadOnly = true; - return; - } - if (updatingFields || ServerPathTextbox.Text.Trim() == Server.GetComponent().ServerDirectory()) - return; - var DialogResult = MessageBox.Show("This will move the entire server installation.", "Confim", MessageBoxButtons.YesNo); - if (DialogResult != DialogResult.Yes) - return; - - if (!Program.CheckAdminWithWarning()) - { - ServerPathTextbox.Enabled = false; - ServerPathTextbox.ReadOnly = true; - return; - } - MessageBox.Show(Server.GetComponent().MoveServer(ServerPathTextbox.Text) ?? "Success!"); - } - - void LoadServerPage() - { - var RepoExists = Server.GetComponent().Exists(); - compileButton.Visible = RepoExists; - AutoUpdateCheckbox.Visible = RepoExists; - initializeButton.Visible = RepoExists; - AutostartCheckbox.Visible = RepoExists; - WebclientCheckBox.Visible = RepoExists; - PortSelector.Visible = RepoExists; - projectNameText.Visible = RepoExists; - CompilerStatusLabel.Visible = RepoExists; - CompileCancelButton.Visible = RepoExists; - CompilerLabel.Visible = RepoExists; - ProjectPathLabel.Visible = RepoExists; - ServerPRLabel.Visible = RepoExists; - ServerGStopButton.Visible = RepoExists; - ServerStartButton.Visible = RepoExists; - ServerGRestartButton.Visible = RepoExists; - ServerRestartButton.Visible = RepoExists; - PortLabel.Visible = RepoExists; - ServerStopButton.Visible = RepoExists; - TestmergeButton.Visible = RepoExists; - ServerTestmergeInput.Visible = RepoExists; - UpdateHardButton.Visible = RepoExists; - UpdateMergeButton.Visible = RepoExists; - UpdateTestmergeButton.Visible = RepoExists; - ResetTestmerge.Visible = RepoExists; - WorldAnnounceField.Visible = RepoExists; - WorldAnnounceButton.Visible = RepoExists; - WorldAnnounceLabel.Visible = RepoExists; - - if (updatingFields) - return; - - var DM = Server.GetComponent(); - var DD = Server.GetComponent(); - var Config = Server.GetComponent(); - var Repo = Server.GetComponent(); - - try - { - updatingFields = true; - - if (!ServerPathTextbox.Focused) - ServerPathTextbox.Text = Config.ServerDirectory(); - - SecuritySelector.SelectedIndex = (int)DD.SecurityLevel(); - - if (!RepoExists) - return; - - var interval = Repo.AutoUpdateInterval(); - var interval_not_zero = interval != 0; - AutoUpdateCheckbox.Checked = interval_not_zero; - AutoUpdateInterval.Visible = interval_not_zero; - AutoUpdateMLabel.Visible = interval_not_zero; - if (interval_not_zero) - AutoUpdateInterval.Value = interval; - - var DaeStat = DD.DaemonStatus(); - var Online = DaeStat == TGDreamDaemonStatus.Online; - ServerStartButton.Enabled = !Online; - ServerGStopButton.Enabled = Online; - ServerGRestartButton.Enabled = Online; - ServerStopButton.Enabled = Online; - ServerRestartButton.Enabled = Online; - - switch (DaeStat) - { - case TGDreamDaemonStatus.HardRebooting: - ServerStatusLabel.Text = "REBOOTING"; - break; - case TGDreamDaemonStatus.Offline: - ServerStatusLabel.Text = "OFFLINE"; - break; - case TGDreamDaemonStatus.Online: - ServerStatusLabel.Text = "ONLINE"; - var pc = DD.PlayerCount(); - if (pc != -1) - ServerStatusLabel.Text += " (" + pc + " players)"; - break; - } - - ServerGStopButton.Checked = DD.ShutdownInProgress(); - - AutostartCheckbox.Checked = DD.Autostart(); - WebclientCheckBox.Checked = DD.Webclient(); - if (!PortSelector.Focused) - PortSelector.Value = DD.Port(); - if (!projectNameText.Focused) - projectNameText.Text = DM.ProjectName(); - - switch (DM.GetStatus()) - { - case TGCompilerStatus.Compiling: - CompilerStatusLabel.Text = "Compiling..."; - compileButton.Enabled = false; - initializeButton.Enabled = false; - CompileCancelButton.Enabled = true; - break; - case TGCompilerStatus.Initializing: - CompilerStatusLabel.Text = "Initializing..."; - compileButton.Enabled = false; - initializeButton.Enabled = false; - CompileCancelButton.Enabled = false; - break; - case TGCompilerStatus.Initialized: - CompilerStatusLabel.Text = "Idle"; - initializeButton.Enabled = true; - compileButton.Enabled = true; - CompileCancelButton.Enabled = false; - break; - case TGCompilerStatus.Uninitialized: - CompilerStatusLabel.Text = "Uninitialized"; - compileButton.Enabled = false; - initializeButton.Enabled = true; - CompileCancelButton.Enabled = false; - break; - default: - CompilerStatusLabel.Text = "Unknown!"; - initializeButton.Enabled = true; - compileButton.Enabled = true; - CompileCancelButton.Enabled = true; - break; - } - var error = DM.CompileError(); - if (error != null) - MessageBox.Show("Error: " + error); - } - finally - { - updatingFields = false; - } - } - - private void ProjectNameText_LostFocus(object sender, EventArgs e) - { - UpdateProjectName(); - } - - void UpdateProjectName() - { - if (!updatingFields) - Server.GetComponent().SetProjectName(projectNameText.Text); - } - - private void PortSelector_ValueChanged(object sender, EventArgs e) - { - if (!updatingFields) - Server.GetComponent().SetPort((ushort)PortSelector.Value); - } - - private void RunServerUpdate(FullUpdateAction fua, ushort tm = 0) - { - if (FullUpdateWorker.IsBusy) - return; - testmergePR = tm; - fuAction = fua; - initializeButton.Enabled = false; - compileButton.Enabled = false; - UpdateHardButton.Enabled = false; - UpdateMergeButton.Enabled = false; - TestmergeButton.Enabled = false; - UpdateTestmergeButton.Enabled = false; - switch (fuAction) - { - case FullUpdateAction.Testmerge: - CompilerStatusLabel.Text = String.Format("Testmerging pull request #{0}...", testmergePR); - break; - case FullUpdateAction.UpdateHard: - CompilerStatusLabel.Text = String.Format("Updating Server (RESET)..."); - break; - case FullUpdateAction.UpdateMerge: - CompilerStatusLabel.Text = String.Format("Updating Server (MERGE)..."); - break; - case FullUpdateAction.UpdateHardTestmerge: - CompilerStatusLabel.Text = String.Format("Updating and testmerging pull request #{0}...", testmergePR); - break; - } - FullUpdateWorker.RunWorkerAsync(); - } - - private void ServerPageRefreshButton_Click(object sender, EventArgs e) - { - LoadServerPage(); - } - - private void InitializeButton_Click(object sender, EventArgs e) - { - if (!Server.GetComponent().Initialize()) - MessageBox.Show("Unable to start initialization!"); - LoadServerPage(); - } - private void CompileButton_Click(object sender, EventArgs e) - { - if (!Server.GetComponent().Compile()) - MessageBox.Show("Unable to start compilation!"); - LoadServerPage(); - } - - private void AutostartCheckbox_CheckedChanged(object sender, System.EventArgs e) - { - if (!updatingFields) - Server.GetComponent().SetAutostart(AutostartCheckbox.Checked); - } - private void ServerStartButton_Click(object sender, System.EventArgs e) - { - if (!ServerStartBGW.IsBusy) - ServerStartBGW.RunWorkerAsync(); - } - - private void ServerStartBGW_DoWork(object sender, DoWorkEventArgs e) - { - try - { - e.Result = Server.GetComponent().Start(); - } - catch (Exception ex) - { - e.Result = ex.ToString(); - } - } - - private void ServerStopButton_Click(object sender, EventArgs e) - { - 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(); - if (res != null) - MessageBox.Show(res); - } - - private void ServerRestartButton_Click(object sender, EventArgs e) - { - var DialogResult = MessageBox.Show("This will immediately restart the server. Continue?", "Confim", MessageBoxButtons.YesNo); - if (DialogResult == DialogResult.No) - return; - var res = Server.GetComponent().Restart(); - if (res != null) - MessageBox.Show(res); - } - - private void ServerGStopButton_Checked(object sender, EventArgs e) - { - if (updatingFields) - return; - 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(); - LoadServerPage(); - } - - private void ServerGRestartButton_Click(object sender, EventArgs e) - { - 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(); - } - - - private void FullUpdateWorker_DoWork(object sender, DoWorkEventArgs e) - { - try - { - var Repo = Server.GetComponent(); - var DM = Server.GetComponent(); - switch (fuAction) - { - case FullUpdateAction.Testmerge: - updateError = Repo.MergePullRequest(testmergePR); - if (updateError == null) - { - Repo.GenerateChangelog(out updateError); - updateError = DM.Compile(true) ? updateError : "Compilation failed!"; - } - break; - case FullUpdateAction.UpdateHard: - updateError = Repo.Update(true); - if (updateError == null) - { - Repo.GenerateChangelog(out updateError); - if (updateError == null) - updateError = Repo.PushChangelog(); - updateError = DM.Compile(true) ? updateError : "Compilation failed!"; - } - break; - case FullUpdateAction.UpdateHardTestmerge: - updateError = Repo.Update(true); - if (updateError == null) - { - Repo.GenerateChangelog(out updateError); - if (updateError == null) - updateError = Repo.PushChangelog(); - updateError = Repo.MergePullRequest(testmergePR); - if (updateError == null) - { - Repo.GenerateChangelog(out updateError); - updateError = DM.Compile(true) ? updateError : "Compilation failed!"; - } - } - break; - case FullUpdateAction.UpdateMerge: - updateError = Repo.Update(false); - if (updateError == null) - { - Repo.GenerateChangelog(out updateError); - if (updateError == null) - Repo.PushChangelog(); //not an error 99% of the time if this fails, just a dirty tree - updateError = DM.Compile(true) ? updateError : "Compilation failed!"; - } - break; - case FullUpdateAction.Reset: - updateError = Repo.Reset(true); - if (updateError == null) - { - Repo.GenerateChangelog(out updateError); - updateError = DM.Compile(true) ? updateError : "Compilation failed!"; - } - break; - } - } - catch (Exception ex) - { - Program.ServiceDisconnectException(ex); - } - } - private void ResetTestmerge_Click(object sender, EventArgs e) - { - RunServerUpdate(FullUpdateAction.Reset); - } - - private void UpdateHardButton_Click(object sender, System.EventArgs e) - { - RunServerUpdate(FullUpdateAction.UpdateHard); - } - - private void UpdateTestmergeButton_Click(object sender, System.EventArgs e) - { - RunServerUpdate(FullUpdateAction.UpdateHardTestmerge, (ushort)ServerTestmergeInput.Value); - } - - private void UpdateMergeButton_Click(object sender, System.EventArgs e) - { - RunServerUpdate(FullUpdateAction.UpdateMerge); - } - private void TestmergeButton_Click(object sender, System.EventArgs e) - { - RunServerUpdate(FullUpdateAction.Testmerge, (ushort)ServerTestmergeInput.Value); - } - - private void SecuritySelector_SelectedIndexChanged(object sender, EventArgs e) - { - if (!updatingFields) - if (!Server.GetComponent().SetSecurityLevel((TGDreamDaemonSecurity)SecuritySelector.SelectedIndex)) - MessageBox.Show("Security change will be applied after next server reboot."); - } - - private void WorldAnnounceButton_Click(object sender, EventArgs e) - { - var msg = WorldAnnounceField.Text; - if (!String.IsNullOrWhiteSpace(msg)) - { - var res = Server.GetComponent().WorldAnnounce(msg); - if (res != null) - { - MessageBox.Show(res); - return; - } - } - WorldAnnounceField.Text = ""; - } - - private void WebclientCheckBox_CheckedChanged(object sender, EventArgs e) - { - if (!updatingFields) - Server.GetComponent().SetWebclient(WebclientCheckBox.Checked); - } - - private void AutoUpdateInterval_ValueChanged(object sender, EventArgs e) - { - if (!updatingFields) - Server.GetComponent().SetAutoUpdateInterval((ulong)AutoUpdateInterval.Value); - } - - private void AutoUpdateCheckbox_CheckedChanged(object sender, EventArgs e) - { - if (updatingFields) - return; - var on = AutoUpdateCheckbox.Visible && AutoUpdateCheckbox.Checked; - AutoUpdateInterval.Visible = on; - AutoUpdateMLabel.Visible = on; - if (!on) - Server.GetComponent().SetAutoUpdateInterval(0); - else - Server.GetComponent().SetAutoUpdateInterval((ulong)AutoUpdateInterval.Value); - } - } -} +using System; +using System.ComponentModel; +using System.Windows.Forms; +using TGServiceInterface; +using TGServiceInterface.Components; + +namespace TGControlPanel +{ + partial class ControlPanel + { + enum FullUpdateAction + { + UpdateHard, + UpdateMerge, + UpdateHardTestmerge, + Reset, + Testmerge, + } + + FullUpdateAction fuAction; + ushort testmergePR; + string updateError; + bool updatingFields = false; + + void InitServerPage() + { + LoadServerPage(); + FullUpdateWorker.RunWorkerCompleted += FullUpdateWorker_RunWorkerCompleted; + projectNameText.LostFocus += ProjectNameText_LostFocus; + projectNameText.KeyDown += ProjectNameText_KeyDown; + ServerStartBGW.RunWorkerCompleted += ServerStartBGW_RunWorkerCompleted; + } + + private void ServerStartBGW_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) + { + if (e.Result != null) + MessageBox.Show((string)e.Result); + LoadServerPage(); + } + + private void ProjectNameText_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Enter) + UpdateProjectName(); + } + + private void FullUpdateWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) + { + if (updateError != null) + MessageBox.Show(updateError); + UpdateHardButton.Enabled = true; + UpdateMergeButton.Enabled = true; + TestmergeButton.Enabled = true; + UpdateTestmergeButton.Enabled = true; + LoadServerPage(); + } + + private void CompileCancelButton_Click(object sender, EventArgs e) + { + var res = Interface.GetComponent().Cancel(); + if (res != null) + MessageBox.Show(res); + LoadServerPage(); + } + + void LoadServerPage() + { + var RepoExists = Interface.GetComponent().Exists(); + compileButton.Visible = RepoExists; + AutoUpdateCheckbox.Visible = RepoExists; + initializeButton.Visible = RepoExists; + AutostartCheckbox.Visible = RepoExists; + WebclientCheckBox.Visible = RepoExists; + PortSelector.Visible = RepoExists; + projectNameText.Visible = RepoExists; + CompilerStatusLabel.Visible = RepoExists; + CompileCancelButton.Visible = RepoExists; + CompilerLabel.Visible = RepoExists; + ProjectPathLabel.Visible = RepoExists; + ServerPRLabel.Visible = RepoExists; + ServerGStopButton.Visible = RepoExists; + ServerStartButton.Visible = RepoExists; + ServerGRestartButton.Visible = RepoExists; + ServerRestartButton.Visible = RepoExists; + PortLabel.Visible = RepoExists; + ServerStopButton.Visible = RepoExists; + TestmergeButton.Visible = RepoExists; + ServerTestmergeInput.Visible = RepoExists; + UpdateHardButton.Visible = RepoExists; + UpdateMergeButton.Visible = RepoExists; + UpdateTestmergeButton.Visible = RepoExists; + ResetTestmerge.Visible = RepoExists; + WorldAnnounceField.Visible = RepoExists; + WorldAnnounceButton.Visible = RepoExists; + WorldAnnounceLabel.Visible = RepoExists; + + if (updatingFields) + return; + + var DM = Interface.GetComponent(); + var DD = Interface.GetComponent(); + var Config = Interface.GetComponent(); + var Repo = Interface.GetComponent(); + + try + { + updatingFields = true; + + ServerPathLabel.Text = "Server Path: " + Interface.GetComponent().ServerDirectory(); + + SecuritySelector.SelectedIndex = (int)DD.SecurityLevel(); + + if (!RepoExists) + return; + + var interval = Repo.AutoUpdateInterval(); + var interval_not_zero = interval != 0; + AutoUpdateCheckbox.Checked = interval_not_zero; + AutoUpdateInterval.Visible = interval_not_zero; + AutoUpdateMLabel.Visible = interval_not_zero; + if (interval_not_zero) + AutoUpdateInterval.Value = interval; + + var DaeStat = DD.DaemonStatus(); + var Online = DaeStat == DreamDaemonStatus.Online; + ServerStartButton.Enabled = !Online; + ServerGStopButton.Enabled = Online; + ServerGRestartButton.Enabled = Online; + ServerStopButton.Enabled = Online; + ServerRestartButton.Enabled = Online; + + switch (DaeStat) + { + case DreamDaemonStatus.HardRebooting: + ServerStatusLabel.Text = "REBOOTING"; + break; + case DreamDaemonStatus.Offline: + ServerStatusLabel.Text = "OFFLINE"; + break; + case DreamDaemonStatus.Online: + ServerStatusLabel.Text = "ONLINE"; + var pc = DD.PlayerCount(); + if (pc != -1) + ServerStatusLabel.Text += " (" + pc + " players)"; + break; + } + + ServerGStopButton.Checked = DD.ShutdownInProgress(); + + AutostartCheckbox.Checked = DD.Autostart(); + WebclientCheckBox.Checked = DD.Webclient(); + if (!PortSelector.Focused) + PortSelector.Value = DD.Port(); + if (!projectNameText.Focused) + projectNameText.Text = DM.ProjectName(); + + switch (DM.GetStatus()) + { + case CompilerStatus.Compiling: + CompilerStatusLabel.Text = "Compiling..."; + compileButton.Enabled = false; + initializeButton.Enabled = false; + CompileCancelButton.Enabled = true; + break; + case CompilerStatus.Initializing: + CompilerStatusLabel.Text = "Initializing..."; + compileButton.Enabled = false; + initializeButton.Enabled = false; + CompileCancelButton.Enabled = false; + break; + case CompilerStatus.Initialized: + CompilerStatusLabel.Text = "Idle"; + initializeButton.Enabled = true; + compileButton.Enabled = true; + CompileCancelButton.Enabled = false; + break; + case CompilerStatus.Uninitialized: + CompilerStatusLabel.Text = "Uninitialized"; + compileButton.Enabled = false; + initializeButton.Enabled = true; + CompileCancelButton.Enabled = false; + break; + default: + CompilerStatusLabel.Text = "Unknown!"; + initializeButton.Enabled = true; + compileButton.Enabled = true; + CompileCancelButton.Enabled = true; + break; + } + var error = DM.CompileError(); + if (error != null) + MessageBox.Show("Error: " + error); + } + finally + { + updatingFields = false; + } + } + + private void ProjectNameText_LostFocus(object sender, EventArgs e) + { + UpdateProjectName(); + } + + void UpdateProjectName() + { + if (!updatingFields) + Interface.GetComponent().SetProjectName(projectNameText.Text); + } + + private void PortSelector_ValueChanged(object sender, EventArgs e) + { + if (!updatingFields) + Interface.GetComponent().SetPort((ushort)PortSelector.Value); + } + + private void RunServerUpdate(FullUpdateAction fua, ushort tm = 0) + { + if (FullUpdateWorker.IsBusy) + return; + testmergePR = tm; + fuAction = fua; + initializeButton.Enabled = false; + compileButton.Enabled = false; + UpdateHardButton.Enabled = false; + UpdateMergeButton.Enabled = false; + TestmergeButton.Enabled = false; + UpdateTestmergeButton.Enabled = false; + switch (fuAction) + { + case FullUpdateAction.Testmerge: + CompilerStatusLabel.Text = String.Format("Testmerging pull request #{0}...", testmergePR); + break; + case FullUpdateAction.UpdateHard: + CompilerStatusLabel.Text = String.Format("Updating Server (RESET)..."); + break; + case FullUpdateAction.UpdateMerge: + CompilerStatusLabel.Text = String.Format("Updating Server (MERGE)..."); + break; + case FullUpdateAction.UpdateHardTestmerge: + CompilerStatusLabel.Text = String.Format("Updating and testmerging pull request #{0}...", testmergePR); + break; + } + FullUpdateWorker.RunWorkerAsync(); + } + + private void ServerPageRefreshButton_Click(object sender, EventArgs e) + { + LoadServerPage(); + } + + private void InitializeButton_Click(object sender, EventArgs e) + { + if (!Interface.GetComponent().Initialize()) + MessageBox.Show("Unable to start initialization!"); + LoadServerPage(); + } + private void CompileButton_Click(object sender, EventArgs e) + { + if (!Interface.GetComponent().Compile()) + MessageBox.Show("Unable to start compilation!"); + LoadServerPage(); + } + + private void AutostartCheckbox_CheckedChanged(object sender, System.EventArgs e) + { + if (!updatingFields) + Interface.GetComponent().SetAutostart(AutostartCheckbox.Checked); + } + private void ServerStartButton_Click(object sender, System.EventArgs e) + { + if (!ServerStartBGW.IsBusy) + ServerStartBGW.RunWorkerAsync(); + } + + private void ServerStartBGW_DoWork(object sender, DoWorkEventArgs e) + { + try + { + e.Result = Interface.GetComponent().Start(); + } + catch (Exception ex) + { + e.Result = ex.ToString(); + } + } + + private void ServerStopButton_Click(object sender, EventArgs e) + { + var DialogResult = MessageBox.Show("This will immediately shut down the server. Continue?", "Confim", MessageBoxButtons.YesNo); + if (DialogResult == DialogResult.No) + return; + var res = Interface.GetComponent().Stop(); + if (res != null) + MessageBox.Show(res); + } + + private void ServerRestartButton_Click(object sender, EventArgs e) + { + var DialogResult = MessageBox.Show("This will immediately restart the server. Continue?", "Confim", MessageBoxButtons.YesNo); + if (DialogResult == DialogResult.No) + return; + var res = Interface.GetComponent().Restart(); + if (res != null) + MessageBox.Show(res); + } + + private void ServerGStopButton_Checked(object sender, EventArgs e) + { + if (updatingFields) + return; + var DialogResult = MessageBox.Show("This will shut down the server when the current round ends. Continue?", "Confim", MessageBoxButtons.YesNo); + if (DialogResult == DialogResult.No) + return; + Interface.GetComponent().RequestStop(); + LoadServerPage(); + } + + private void ServerGRestartButton_Click(object sender, EventArgs e) + { + var DialogResult = MessageBox.Show("This will restart the server when the current round ends. Continue?", "Confim", MessageBoxButtons.YesNo); + if (DialogResult == DialogResult.No) + return; + Interface.GetComponent().RequestRestart(); + } + + + private void FullUpdateWorker_DoWork(object sender, DoWorkEventArgs e) + { + try + { + var Repo = Interface.GetComponent(); + var DM = Interface.GetComponent(); + switch (fuAction) + { + case FullUpdateAction.Testmerge: + updateError = Repo.MergePullRequest(testmergePR); + if (updateError == null) + { + Repo.GenerateChangelog(out updateError); + updateError = DM.Compile(true) ? updateError : "Compilation failed!"; + } + break; + case FullUpdateAction.UpdateHard: + updateError = Repo.Update(true); + if (updateError == null) + { + Repo.GenerateChangelog(out updateError); + if (updateError == null) + updateError = Repo.SynchronizePush(); + updateError = DM.Compile(true) ? updateError : "Compilation failed!"; + } + break; + case FullUpdateAction.UpdateHardTestmerge: + updateError = Repo.Update(true); + if (updateError == null) + { + Repo.GenerateChangelog(out updateError); + if (updateError == null) + updateError = Repo.SynchronizePush(); + updateError = Repo.MergePullRequest(testmergePR); + if (updateError == null) + { + Repo.GenerateChangelog(out updateError); + updateError = DM.Compile(true) ? updateError : "Compilation failed!"; + } + } + break; + case FullUpdateAction.UpdateMerge: + updateError = Repo.Update(false); + if (updateError == null) + { + Repo.GenerateChangelog(out updateError); + if (updateError == null) + Repo.SynchronizePush(); //not an error 99% of the time if this fails, just a dirty tree + updateError = DM.Compile(true) ? updateError : "Compilation failed!"; + } + break; + case FullUpdateAction.Reset: + updateError = Repo.Reset(true); + if (updateError == null) + { + Repo.GenerateChangelog(out updateError); + updateError = DM.Compile(true) ? updateError : "Compilation failed!"; + } + break; + } + } + catch (Exception ex) + { + Program.ServiceDisconnectException(ex); + } + } + private void ResetTestmerge_Click(object sender, EventArgs e) + { + RunServerUpdate(FullUpdateAction.Reset); + } + + private void UpdateHardButton_Click(object sender, System.EventArgs e) + { + RunServerUpdate(FullUpdateAction.UpdateHard); + } + + private void UpdateTestmergeButton_Click(object sender, System.EventArgs e) + { + RunServerUpdate(FullUpdateAction.UpdateHardTestmerge, (ushort)ServerTestmergeInput.Value); + } + + private void UpdateMergeButton_Click(object sender, System.EventArgs e) + { + RunServerUpdate(FullUpdateAction.UpdateMerge); + } + private void TestmergeButton_Click(object sender, System.EventArgs e) + { + RunServerUpdate(FullUpdateAction.Testmerge, (ushort)ServerTestmergeInput.Value); + } + + private void SecuritySelector_SelectedIndexChanged(object sender, EventArgs e) + { + if (!updatingFields) + if (!Interface.GetComponent().SetSecurityLevel((DreamDaemonSecurity)SecuritySelector.SelectedIndex)) + MessageBox.Show("Security change will be applied after next server reboot."); + } + + private void WorldAnnounceButton_Click(object sender, EventArgs e) + { + var msg = WorldAnnounceField.Text; + if (!String.IsNullOrWhiteSpace(msg)) + { + var res = Interface.GetComponent().WorldAnnounce(msg); + if (res != null) + { + MessageBox.Show(res); + return; + } + } + WorldAnnounceField.Text = ""; + } + + private void WebclientCheckBox_CheckedChanged(object sender, EventArgs e) + { + if (!updatingFields) + Interface.GetComponent().SetWebclient(WebclientCheckBox.Checked); + } + + private void AutoUpdateInterval_ValueChanged(object sender, EventArgs e) + { + if (!updatingFields) + Interface.GetComponent().SetAutoUpdateInterval((ulong)AutoUpdateInterval.Value); + } + + private void AutoUpdateCheckbox_CheckedChanged(object sender, EventArgs e) + { + if (updatingFields) + return; + var on = AutoUpdateCheckbox.Visible && AutoUpdateCheckbox.Checked; + AutoUpdateInterval.Visible = on; + AutoUpdateMLabel.Visible = on; + if (!on) + Interface.GetComponent().SetAutoUpdateInterval(0); + else + Interface.GetComponent().SetAutoUpdateInterval((ulong)AutoUpdateInterval.Value); + } + } +} diff --git a/TGControlPanel/StaticPage.cs b/TGControlPanel/ControlPanel/StaticPage.cs similarity index 89% rename from TGControlPanel/StaticPage.cs rename to TGControlPanel/ControlPanel/StaticPage.cs index 3f914afabc..1f17132820 100644 --- a/TGControlPanel/StaticPage.cs +++ b/TGControlPanel/ControlPanel/StaticPage.cs @@ -3,10 +3,11 @@ using System.Collections.Generic; using System.IO; using System.Windows.Forms; using TGServiceInterface; +using TGServiceInterface.Components; namespace TGControlPanel { - partial class Main + partial class ControlPanel { IDictionary IndexesToPaths = new Dictionary(); IList EnumeratedPaths = new List() { "" }; @@ -22,7 +23,7 @@ namespace TGControlPanel void InitStaticPage() { - if(!Server.AuthenticateAdmin()) + if(!Interface.ConnectToInstance().HasFlag(ConnectivityLevel.Administrator)) 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) { @@ -324,19 +325,19 @@ namespace TGControlPanel private void RecreateStaticButton_Click(object sender, EventArgs e) { - if (!Program.CheckAdminWithWarning()) + if (!CheckAdminWithWarning()) { RecreateStaticButton.Visible = false; return; } if (MessageBox.Show("This will rename the current static directory to a backup and recreate it. Continue?", "Confirm", MessageBoxButtons.YesNo) != DialogResult.Yes) return; - if (!Program.CheckAdminWithWarning()) + if (!CheckAdminWithWarning()) { 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/CountedForm.cs b/TGControlPanel/CountedForm.cs new file mode 100644 index 0000000000..064c2f9b23 --- /dev/null +++ b/TGControlPanel/CountedForm.cs @@ -0,0 +1,35 @@ +using System.Windows.Forms; + +namespace TGControlPanel +{ + /// + /// Calls when all s are d + /// + class CountedForm : Form + { + /// + /// The current number of active s + /// + static uint FormCount; + + /// + /// Construct a . Increments + /// + public CountedForm() + { + FormClosed += CountedForm_FormClosed; + ++FormCount; + } + + /// + /// Decrements . Calls if it reaches 0 + /// + /// The sender of the event + /// The + private void CountedForm_FormClosed(object sender, FormClosedEventArgs e) + { + if (--FormCount == 0) + Application.Exit(); + } + } +} diff --git a/TGControlPanel/InstanceSelector.Designer.cs b/TGControlPanel/InstanceSelector.Designer.cs new file mode 100644 index 0000000000..4b07446a17 --- /dev/null +++ b/TGControlPanel/InstanceSelector.Designer.cs @@ -0,0 +1,134 @@ +namespace TGControlPanel +{ + partial class InstanceSelector + { + /// + /// 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)) + { + Cleanup(); + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(InstanceSelector)); + this.InstanceListBox = new System.Windows.Forms.ListBox(); + this.CreateInstanceButton = new System.Windows.Forms.Button(); + this.ImportInstanceButton = new System.Windows.Forms.Button(); + this.RenameInstanceButton = new System.Windows.Forms.Button(); + this.DetachInstanceButton = new System.Windows.Forms.Button(); + this.RefreshButton = new System.Windows.Forms.Button(); + this.SuspendLayout(); + // + // InstanceListBox + // + this.InstanceListBox.Anchor = System.Windows.Forms.AnchorStyles.Left; + this.InstanceListBox.FormattingEnabled = true; + this.InstanceListBox.Location = new System.Drawing.Point(13, 13); + this.InstanceListBox.Name = "InstanceListBox"; + this.InstanceListBox.Size = new System.Drawing.Size(339, 238); + this.InstanceListBox.TabIndex = 0; + // + // CreateInstanceButton + // + this.CreateInstanceButton.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.CreateInstanceButton.Location = new System.Drawing.Point(358, 15); + this.CreateInstanceButton.Name = "CreateInstanceButton"; + this.CreateInstanceButton.Size = new System.Drawing.Size(148, 25); + this.CreateInstanceButton.TabIndex = 14; + this.CreateInstanceButton.Text = "Create Instance"; + this.CreateInstanceButton.UseVisualStyleBackColor = true; + this.CreateInstanceButton.Click += new System.EventHandler(this.CreateInstanceButton_Click); + // + // ImportInstanceButton + // + this.ImportInstanceButton.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.ImportInstanceButton.Location = new System.Drawing.Point(358, 45); + this.ImportInstanceButton.Name = "ImportInstanceButton"; + this.ImportInstanceButton.Size = new System.Drawing.Size(148, 25); + this.ImportInstanceButton.TabIndex = 15; + this.ImportInstanceButton.Text = "Import Instance"; + this.ImportInstanceButton.UseVisualStyleBackColor = true; + this.ImportInstanceButton.Click += new System.EventHandler(this.ImportInstanceButton_Click); + // + // RenameInstanceButton + // + this.RenameInstanceButton.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.RenameInstanceButton.Location = new System.Drawing.Point(358, 76); + this.RenameInstanceButton.Name = "RenameInstanceButton"; + this.RenameInstanceButton.Size = new System.Drawing.Size(148, 25); + this.RenameInstanceButton.TabIndex = 16; + this.RenameInstanceButton.Text = "Rename Instance"; + this.RenameInstanceButton.UseVisualStyleBackColor = true; + this.RenameInstanceButton.Click += new System.EventHandler(this.RenameInstanceButton_Click); + // + // DetachInstanceButton + // + this.DetachInstanceButton.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.DetachInstanceButton.Location = new System.Drawing.Point(358, 107); + this.DetachInstanceButton.Name = "DetachInstanceButton"; + this.DetachInstanceButton.Size = new System.Drawing.Size(148, 25); + this.DetachInstanceButton.TabIndex = 17; + this.DetachInstanceButton.Text = "Detach Instance"; + this.DetachInstanceButton.UseVisualStyleBackColor = true; + this.DetachInstanceButton.Click += new System.EventHandler(this.DetachInstanceButton_Click); + // + // RefreshButton + // + this.RefreshButton.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.RefreshButton.Location = new System.Drawing.Point(358, 226); + this.RefreshButton.Name = "RefreshButton"; + this.RefreshButton.Size = new System.Drawing.Size(148, 25); + this.RefreshButton.TabIndex = 19; + this.RefreshButton.Text = "Refresh"; + this.RefreshButton.UseVisualStyleBackColor = true; + this.RefreshButton.Click += new System.EventHandler(this.RefreshButton_Click); + // + // InstanceSelector + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + 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(518, 261); + this.Controls.Add(this.RefreshButton); + this.Controls.Add(this.DetachInstanceButton); + this.Controls.Add(this.RenameInstanceButton); + this.Controls.Add(this.ImportInstanceButton); + this.Controls.Add(this.CreateInstanceButton); + this.Controls.Add(this.InstanceListBox); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.Name = "InstanceSelector"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "Server Instances"; + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.ListBox InstanceListBox; + private System.Windows.Forms.Button CreateInstanceButton; + private System.Windows.Forms.Button ImportInstanceButton; + private System.Windows.Forms.Button RenameInstanceButton; + private System.Windows.Forms.Button DetachInstanceButton; + private System.Windows.Forms.Button RefreshButton; + } +} \ No newline at end of file diff --git a/TGControlPanel/InstanceSelector.cs b/TGControlPanel/InstanceSelector.cs new file mode 100644 index 0000000000..fa6ae73761 --- /dev/null +++ b/TGControlPanel/InstanceSelector.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Windows.Forms; +using TGServiceInterface; + +namespace TGControlPanel +{ + /// + /// Form used for managing manipulation functions + /// + partial class InstanceSelector : CountedForm + { + /// + /// The we build instance connections from + /// + readonly Interface masterInterface; + /// + /// List of from + /// + IList InstanceData; + public InstanceSelector(Interface I) + { + InitializeComponent(); + InstanceListBox.MouseDoubleClick += InstanceListBox_MouseDoubleClick; + masterInterface = I; + RefreshInstances(); + } + + /// + /// Used to wrap calls in a non-blocking fashion + /// + /// The operation to wrap + Task WrapServerOp(Action action) + { + Enabled = false; + UseWaitCursor = true; + try + { + return Task.Factory.StartNew(action); + } + finally + { + Enabled = true; + UseWaitCursor = false; + } + } + + /// + /// Connects to a if it is double clicked in + /// + /// The sender of the event + /// The + void InstanceListBox_MouseDoubleClick(object sender, MouseEventArgs e) + { + var index = InstanceListBox.IndexFromPoint(e.Location); + if (index != ListBox.NoMatches) + TryConnectToInstance(InstanceData[index].Name); + } + + /// + /// Returns the associated with 's current selected index + /// + /// The associated with 's current selected index if it exists, otherwise + InstanceMetadata GetSelectedInstanceMetadata() + { + var index = InstanceListBox.SelectedIndex; + return index != ListBox.NoMatches ? InstanceData[index] : null; + } + + /// + /// Called by + /// + void Cleanup() + { + masterInterface.Dispose(); + } + + /// + /// Loads the using + /// + async void RefreshInstances() + { + InstanceListBox.Items.Clear(); + await WrapServerOp(() => { + InstanceData = masterInterface.GetService().ListInstances(); + }); + foreach(var I in InstanceData) + InstanceListBox.Items.Add(String.Format("{0}: {1} - {2} - {3}", I.LoggingID, I.Name, I.Path, I.Enabled ? "ONLINE" : "OFFLINE")); + } + + /// + /// Tries to start a for a given + /// + /// The name of the to connect to + async void TryConnectToInstance(string instanceName) + { + if(ControlPanel.InstancesInUse.TryGetValue(instanceName, out ControlPanel activeCP)) + { + activeCP.BringToFront(); + return; + } + var InstanceAccessor = new Interface(masterInterface); + try + { + ConnectivityLevel res = ConnectivityLevel.None; + await WrapServerOp(() => { res = InstanceAccessor.ConnectToInstance(instanceName); }); + if (!res.HasFlag(ConnectivityLevel.Connected)) + { + MessageBox.Show("Unable to connect to instance! Does it exist?"); + RefreshInstances(); + } + else if (!res.HasFlag(ConnectivityLevel.Authenticated)) + MessageBox.Show("The current user is not authorized to access this instance!"); + else + new ControlPanel(InstanceAccessor).Show(); + } + catch + { + InstanceAccessor.Dispose(); + throw; + } + } + + /// + /// Prompts the user for parameters to + /// + /// The sender of the event + /// The + async void DetachInstanceButton_Click(object sender, EventArgs e) + { + var imd = GetSelectedInstanceMetadata(); + if (imd == null) + return; + if (MessageBox.Show(String.Format("This will dissociate the server instance at \"{0}\"! Are you sure?", imd.Path), "Instance Detach", MessageBoxButtons.YesNo) != DialogResult.Yes) + return; + string res = null; + await WrapServerOp(() => { res = masterInterface.GetService().DetachInstance(imd.Name); }); + if (res != null) + MessageBox.Show(res); + RefreshInstances(); + } + + /// + /// Calls + /// + /// The sender of the event + /// The + void RefreshButton_Click(object sender, EventArgs e) + { + RefreshInstances(); + } + + /// + /// Prompts the user for parameters to + /// + /// The sender of the event + /// The + async void RenameInstanceButton_Click(object sender, EventArgs e) + { + var imd = GetSelectedInstanceMetadata(); + if (imd == null) + return; + var new_name = Program.TextPrompt("Instance Rename", "Enter a new name for the instance:"); + if (new_name == null) + return; + if (imd.Enabled && MessageBox.Show(String.Format("This will temporarily offline the server instance! Are you sure?", imd.Path), "Instance Restart", MessageBoxButtons.YesNo) != DialogResult.Yes) + return; + string res = null; + await WrapServerOp(() => { res = masterInterface.GetService().RenameInstance(imd.Name, new_name); }); + if (res != null) + MessageBox.Show(res); + RefreshInstances(); + } + + /// + /// Prompts the user for parameters to + /// + /// The sender of the event + /// The + async void ImportInstanceButton_Click(object sender, EventArgs e) + { + var instance_path = Program.TextPrompt("Instance Import", "Enter the full path to the instance:"); + if (instance_path == null) + return; + string res = null; + await WrapServerOp(() => { res = masterInterface.GetService().ImportInstance(instance_path); }); + if (res != null) + MessageBox.Show(res); + RefreshInstances(); + } + + /// + /// Prompts the user for parameters to + /// + /// The sender of the event + /// The + async void CreateInstanceButton_Click(object sender, EventArgs e) + { + var instance_name = Program.TextPrompt("Instance Creation", "Enter the name of the instance:"); + if (instance_name == null) + return; + var instance_path = Program.TextPrompt("Instance Creation", "Enter the full path to the instance:"); + if (instance_path == null) + return; + string res = null; + await WrapServerOp(() => { res = masterInterface.GetService().CreateInstance(instance_name, instance_path); }); + if (res != null) + MessageBox.Show(res); + RefreshInstances(); + } + } +} diff --git a/TGControlPanel/InstanceSelector.resx b/TGControlPanel/InstanceSelector.resx new file mode 100644 index 0000000000..a83f34c04b --- /dev/null +++ b/TGControlPanel/InstanceSelector.resx @@ -0,0 +1,2179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + AAABAAUAEBAAAAEAIABoBAAAVgAAABgYAAABACAAiAkAAL4EAAAgIAAAAQAgAKgQAABGDgAAMDAAAAEA + IACoJQAA7h4AAKCgAAABACAAqJwBAJZEAAAoAAAAEAAAACAAAAABACAAAAAAAAAEAAATCwAAEwsAAAAA + AAAAAAAAAAAAAAAAAABeTTgAWkgyA1pIMilOOyMoRjMaRz4qEMM+KhDESDUcSU07IyhaSDIpWUgxA15N + NwAAAAAAAAAAAAAAAADSzsgAPyoQAEk2HTk9KQ7ZOSUK5DciB+43Igf/OCQJ/zkkCe46JQrkPSgO2UYz + Gjk8JwwA0s3HAAAAAABVQywAbV1JAk47IytALBK6XEo0/2xcSP9lVUD/mo+C/5CEdf9jUz7/ZFQ//2BP + Ov9LOCC6SzgfK2hYRAJSPygA////AUg1HHA7JgzkOSQJ/29gTP9wYU7/6+nm/+rn5f9rXEj/U0Ap/1NB + Kv9YRzD/Z1ZC/zsmC+RINBtw////AYFzYgU/KhCxOiUK/4V4aP+hl4r/ZFQ///b19P/a1tH/c2RR/87J + w//X083/Y1M+/3BhTv8+Kg//PyoQsYFzYgVYRi8cPSkOz0MvFf/a1tH/vreu/2RTPv/08/H//Pz7//Py + 8P/+/f3/+vn5/3JjUP+Hemr/RjMa/z0pDs9XRS8cQi4VnDciB/xsXEn/9PPx/7y1rP9kUz7/9PLx//// + //////////////f29f9qW0f/oJaJ/3BhTf83Igf8Qi4VnD8qEN45JAn/saie//////+7tKv/ZFM+//Ty + 8f//////7u3q/5SJev95a1n/Y1M9/9rX0v+zq6D/OSQJ/z8qEN5EMBeFNiEF/oV4aP//////vLSr/2RT + Pv/08vH//////+7t6v+UiXr/eWtZ/2NTPv/X0s3/h3pq/zYhBf5EMBeFRzMaZTYhBfuAc2L//////7y0 + q/9kUz7/9PLx//////////////////f29f9qWkX/rqaa/4R3Zv82IQX7RzMaZUUxGIc3Igf/X085/97b + 1/+9tqz/ZFM+//Tz8f/8/Pv/8/Lw//79/f/6+fn/cWJP/5KHeP9kUz7/NyIH/0UxGIdMOSE0PikPvzkk + Cf+Lf2//in5u/2JSPf/29fT/2tbR/3JkUf/PycP/19PN/2NTPv9oWEP/OycM/z0pD79LOCA0RDEYAE89 + JSU/KxHWcGBN/1NAKf9wYU3/9vX0/+rn5P9pWkX/VEIq/1NBKv9YRzD/ZlZB/zwnDdZPPSUlRTEYAGdY + QwAAAAAAQCwSkkw5If9cSjT/bF1J/6ujl/+lm4//j4N0/4R3Z/9lVUD/YE86/0IuFP8/KxGSAAAAAGdY + QwAAAAAAQy8WAFA+JiFDLxZmPCcMnzciBvM5JAn/NyIH/zgjCP87Jgz/OCMI8zwnDZ9DLxVmTzwlIUEs + EwAAAAAAAAAAAAAAAAAAAAAAQi4VAEYyGQc+KQ+GPSgO6EEtE61CLhStPSkP6D0pDoZEMBcHQS0TAAAA + AAAAAAAAAAAAAOAHAADgBwAAgAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAB + AADAAwAAwAMAAPAPAAAoAAAAGAAAADAAAAABACAAAAAAAAAJAAATCwAAEwsAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAP///wD///8A////AP///wBVQy0AemxaA047I0NCLhTGQS0TxlE+J0aFd2cEYlI9AP// + /wD///8A////AP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF5NNwBhUDsMTToifk47 + I50+KQ+BQy8WmzwnDeg4Iwj/OCMI/z0oDupDLxacPikPgU47I51NOiF+YFA6DF1NNwAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAj4R1AB8HAABLOCBgOiUK+DYhBf82IQX/OCMI/zciB/82IQX/NiEF/zUg + Bf82IQX/NiEF/zYhBf86JQr4SzgfYB8HAACOgnMAAAAAAAAAAAAAAAAAAAAAAAAAAACMgXIACAAAAGBP + OS0/KxHVRzMa/2BPOf9eTTf/QSwT/0UyGf+Lf2//joJy/15NN/9fTjn/X085/11MNv9KNx//PysR1V9O + OC0GAAAAjIFyAAAAAAAAAAAAnpWIANrY0wFaSTIhRTIZdUAsEtc2IQb/d2lW/35xX/+Lf2//vLWs/8/K + xP/7+vr/wLqx/3NkUf9qWkb/alpG/25fS/+GeWj/ZFM+/z8qENdFMRh1WEYvIdTOyQGZjX8AbF1KAG1e + ShxFMhjMOSQJ/jciB/8zHgL/dmhV/1E/KP9oWET/+Pf2///////h3tr/UD4m/zsnDP9EMBb/QzAW/zsm + C/9BLRT/hnlp/0EtFP84Iwj+RTEYzG1dShxsXUkATDkgAE06Iig7JgzoNyIH/z4pD/9wYU3/ubKo/1RB + Kv9qWkb/+Pf3///////Hwbn/OyYL/4p+bv/c2NT/3NjT/6iek/87Jwz/e21b/1E+J/82IQb/OyYM6E06 + IihMOSAAWUcwAF1MNjs9KQ/0NSAE/2pbRv/z8vD/7evo/1JAKf9qWkb/+Pf3///////p5+T/tq6l/9vX + 0v///////////9XRy/9BLRP/in1t/25fS/81HwT/PSkP9F5NNztZRzEAAAAAAEo3H3A6JQr9NB8D/3xu + XP//////7Oro/1JAKf9qWkb/+Pf3/////////////////////////////////9XRy/9BLBP/kIR1/35w + Xv80HwP/OiUK/Uo3HnAAAAAAUkApRj4pD+M3Igf/OiUK/6iekv//////7Oro/1JAKf9qWkb/+Pf3//// + /////////////////////////////9XQy/9ALBL/k4d5/6qhlf86JQr/NyIH/z4pD+NSPyhGQy8V1jgj + CP82IQb/emxa//Tz8f//////7Oro/1JAKf9qWkb/+Pf3//////////////////f29f/x7+3/8vDu/6yj + mP83Igf/mI6A//b29P97bVv/NiEG/zgjCP9DLxXWRTEYxDgjCP82IQb/m5CD////////////7Oro/1JA + Kf9qWkb/+Pf3/////////////////6ackP9XRS7/WEYw/0IuFf91Z1T/4d7Z//////+bkIP/NiEG/zgj + CP9FMRjEUT4nXTsmDPU2IAX/XUw2/+3r6f//////7Oro/1JAKf9qWkb/+Pf3/////////////////6ac + kP9XRS7/WEYv/0IuFP92Z1T/4t/b/+7s6v9dTDb/NiAF/zsmDPVRPiddYlE7Gz8qENQ3Igb/SDUc/93a + 1f//////7Oro/1JAKf9qWkb/+Pf3//////////////////f29f/x7+3/8vDu/6yjmP83Igf/mo+B/9/c + 2P9INRz/NyIG/z8qENRhUTwbX044Jj8qEOA2IAX/WUcx/+3r6f//////7Oro/1JAKf9qWkb/+Pf3//// + /////////////////////////////9XQy/9ALBL/kod4/+/t6/9ZRzD/NiAF/z8qEOBfTjgmVEIqRjwn + DfI3IQb/TDkg/8bAuf//////7Oro/1JAKf9qWkb/+Pf3/////////////////////////////////9XR + y/9BLBP/kYV2/8nDvP9MOSH/NiEG/zwnDfJUQitGVUMsQj4pD+Q4Iwj/NiEG/047I//PysT/7uzq/1JA + Kf9qWkb/+Pf3///////p5+T/tq6l/9vX0v///////////9XRy/9CLhT/fG5c/1tKNP82IQX/OCMI/z4p + D+RVQyxCwry1Ak07I0Y/KhDUNyIH/0s4IP+akIL/jIBw/0UxGP9rW0f/+Pf3///////Fv7j/OiYL/4p+ + bv/c2NT/3NjT/6iek/87Jwz/eWpY/006Iv83Igf/PioQ1E06Ika8ta0CY1M+AEIvFQBYRjA1PCcN3lA9 + Jf96a1n/NyIH/zQeA/9qW0b/+Pf2///////e29b/TToi/zsmC/9DLxb/QzAW/zsmC/9BLRT/hnlp/0Et + FP89KA7eV0YvNUMwFgBhUDwAAAAAAG5fSwD5+PcBQCwSm0QwF/+FeGj/gnVk/3lrWf+Vinz/9fPy//// + ///39vX/ubGo/4F0Y/9yYk//alpG/25fS/+GeWj/Y1M9/zciB/9BLhSb+fr5AW5gTAAAAAAAAAAAAAAA + AABBLRMASjcfVjsmDPs7Jgv/Qy8W/047I/9aSDL/hHdm/6CWif9xYk7/cWJP/52Thv+DdWX/Xk04/11M + Nv9KNx//OCMH/zsmDPtKNx9WQS0TAAAAAAAAAAAAAAAAAAAAAABcSzUAY1I9E006ImxFMRieQCsSxzgj + CPU2IAX/NiEG/zYhBv81IAT/NSAE/zYhBv82IQb/NSAF/zciB/U+KhDHQy8Wnko3H2xiUTwTXEo0AAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAKGXiwDJxL0BalpGDT4qEHM5JQrzOCMI/zgjCP86Jgv7OyYL+zgj + CP84Iwj/OSQK8z4pD3NnV0MNt66lAZKGeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAASzggAFNBKgVBLRSBPysR60QwFrBNOyNpTzwlaUYyGbBALBLrQS0TgVA+JgVJNh0AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA/4H/APgAHwD4AB8A8AAPAIAAAQCAAAEAgAABAIAAAQCAAAEAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAADAMAAAwDgAAcA4AAHAPgAHwD+AH8AKAAAACAA + AABAAAAAAQAgAAAAAAAAEAAAEwsAABMLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAHlrWQCGeWkEVkQtRkUyGclEMBfJU0EpR52UhwZ7blwAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAalpHAAAAAABlVUAmcGBNQG5fTDE8Jw0dXEs1L0g1HIw9KQ7qOCMI/zgjCP8+KQ/sTToikltK + My88Jw0dbl9MMXBgTUBlVUAmAAAAAGdXQgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAABaSDIAY1I9IEQwF8pBLRP5PysR7TkkCeA+KQ/rOSUK/jgjCP84Iwj/OCMI/zgj + CP85JAr+PSkP6zkkCeA/KxHtQS0T+UQwF8piUTwgWUgxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAeGpXALy2rQJKNx+MOSQK/zchBv83Igb/NyIH/zgiB/84Iwj/NyIH/zUg + Bf81IAX/NyIH/zciB/83Igf/NyIH/zciBv83Igf/OSQK/0o3Hoy6s6oCdmhWAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAANnUzwA0HwMAXk44Rz4pD+05JQr/RTEY/0g1HP9GMhn/OSQJ/zUf + BP9AKxH/e21b/3xuXP9HMxr/RjIZ/0g0HP9INBv/RzQb/0EtFP83Igf/PSgO7V5NN0czHgIA2dTPAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAJ2ThgAWAAAAYlE8ElRCK2BFMRfVNiEG/1xLNf+pn5T/lIl7/6ee + kv+DdmX/bl9L/763rv/5+Pf/8vHv/66mm/+Wi33/lop8/5aKfP+Wi33/mo+C/4l9bf9HNBv/Qy8W1VRC + K2BhUDsSJxAAAI+CcwAAAAAAAAAAAAAAAAByY1AAg3ZlFVE/J2lALBLFOyYM+jgjCP81IAT/a1tH/4Z5 + af83Igf/e21b//X08//39vX///////v6+f+aj4L/STYd/z4pD/89KA7/PSgO/z0pDv9DLxb/dGZT/5WK + e/8+KQ//OyYL+j8rEcVQPSVpgHJhFW5eSwAAAAAAAAAAAEw6IgBgTzpiPioQ/jgjCP84Iwj/OCMI/zMe + Av9pWUT/g3Zl/zEcAP93aVb//Pv7////////////1tLN/0UyGf83Igf/Qi4U/0MvFv9DLxb/QCwS/zci + B/83IQb/kIR1/15NN/82IAX/OCMI/z4qEP5gTzliTDkhAAAAAAAAAAAAPCgNAEMvFWk5JAn/OCMI/zgj + CP86JQr/V0Uv/66lmv+Ie2v/MRwA/3dpVv/8+/v///////////+5sqn/NSAF/0k2Hv/DvLT/1tHM/9XR + y//Mx8D/Z1dD/zMdAv92aFX/cWJP/zUgBP84Iwj/OSQJ/0MvFmk8KA0AAAAAAAAAAAAAAAAAU0AphDkk + Cf84Iwj/OCIH/4x/cP/q5+T//////4p+bv8xHAD/d2lW//z7+////////////8fBuv9oWUT/fW9d/+7s + 6v////////////////+Wi3z/Mh0B/3VmU/+YjX//NyIH/zgjCP85JAn/U0EphAAAAAAAAAAAemlYAAAA + AABPPSWYOSQJ/zgjCP87Jgv/vbat////////////in1u/zEcAP93aVb//Pv7/////////////Pz8//j3 + 9v/5+Pj//v7+/////////////////5iMf/8yHQH/d2hW/7ixp/87Jgv/OCMI/zkkCf9PPSWYAAAAAHRm + UgBcSzUAZVQ/J0EtE9M4Iwj/OCMI/z0pDv/JxL3///////////+KfW7/MRwA/3dpVv/8+/v///////// + ////////////////////////////////////////mIx+/zIdAf93aFb/xb+3/z0pD/84Iwj/OCMI/0Et + E9NjUz4nW0kzAHlrWBBJNh2oOSQJ/zgjCP82IQb/U0Ap/+He2v///////////4p9bv8xHAD/d2lW//z7 + +/////////////////////////////////////////////////+YjH7/Mh0B/3VnVP/d2dX/U0Ep/zYh + Bv84Iwj/OSQJ/0g1HKh3aVcQTjsjjDsmC/k4Iwj/NyIH/0AsEv+zq6H/////////////////in1u/zEc + AP93aVb//Pv7/////////////////////////////////////////////////4t/b/8yHAD/c2RR//v7 + +v+0raP/QCwT/zciB/84Iwj/OyYL+U47I4xFMRjoOCMH/zgjCP82IAX/hHdm//r6+f////////////// + //+KfW7/MRwA/3dpVv/8+/v///////////////////////f29f/Szsj/zcjC/87Jw/+wqJ7/Tzwk/zUf + BP+MgHD///7+//v6+v+Ed2f/NiAF/zgjCP84Iwf/RTEY6E48JJs5JAn/OCMI/zUgBf97bVv/+vn5//// + /////////////4p9bv8xHAD/d2lW//z7+///////////////////////4d7Z/1A+Jv89KQ//PioQ/zkk + Cv89KQ//nJGE/+zq5///////+vn5/3ttW/81IAX/OCMI/zkkCf9OPCSbYlI9O0AsEuk4Iwf/NyIH/0Yy + Gf/Tzsj/////////////////in1u/zEcAP93aVb//Pv7///////////////////////h3tn/UD4m/z0p + D/8+KhD/OSQJ/z0pD/+ckYT/7Oro///////Tzsj/RjIZ/zciB/84Iwf/QCwS6WJSPTuooJQFRTEYoDgj + CP84Iwj/NiEG/6KYi/////////////////+KfW7/MRwA/3dpVv/8+/v///////////////////////f2 + 9f/Szsj/zcjC/87Jw/+wqJ3/Tzwk/zUfBP+MgHD//////6KYi/82IQX/OCMI/zgjCP9FMRigqJ+TBQAA + AABOOyOLOSQJ/zgjCP84Iwj/sKid/////////////////4p9bv8xHAD/d2lW//z7+/////////////// + //////////////////////////////////+Lf2//MhwA/3NkUf/+/v7/sKid/zgjCP84Iwj/OSQJ/048 + I4sAAAAAycO7BUk1Ha04Iwj/OCIH/0ArEf/MxsD/////////////////in1u/zEcAP93aVb//Pv7//// + /////////////////////////////////////////////5iMf/8yHQH/dGVS//79/f/Mx8D/QCsR/zgi + B/84Iwj/STYdrcjEvAV8b10UQzAWzDgjCP84Iwf/PikP/62kmf/9/Pz///////////+KfW7/MRwA/3dp + Vv/8+/v/////////////////////////////////////////////////mIx+/zIdAf90ZVL/+fj3/66m + m/8+KQ//OCMH/zgjCP9EMBbMfW9eFGVVQCZBLBPhNyIH/zgjCP83Igf/Qy8V/5yShf/18/L//////4p9 + bv8xHAD/d2lW//z7+/////////////z8/P/49/b/+fj4//7+/v////////////////+YjH//Mh0B/3Rl + Uv+nnZH/Qy8W/zciB/84Iwj/NyIH/0EsE+FlVUAmhHZmDE48JIE8Jw3zOCMI/zgjCP81IAX/VkQt/9bS + zf/+/v7/in1t/zEcAP93aVb//Pv7////////////x8G6/2hZRP99b13/7uzq/////////////////5aL + fP8yHQH/cWJO/3RlUv80HwP/OCMI/zgjCP88Jw3zTTsjgYF0YwxjUj0AfG5dBE88JV4/KxHkOCMI/zUg + BP91Z1T/kIV2/3NlUv9XRS//Mx4C/3dpVv/8+/v///////////+3sKb/NSAE/0k2Hv/DvLT/1tHM/9XR + y//Mx8D/Z1dD/zMdAv92aFX/cmNP/zUgBP84Iwj/PyoQ5E48JF58blwEY1I9AAAAAAByZVEA////AF5N + OEk+KhDmNR8E/3ZnVP9tXUn/LxkA/zUfBP80HgP/d2hW//z7+////////////9LNx/9CLhX/OCMI/0Iu + FP9DLxb/Qy8W/0AsEv83Igf/NyEG/5CEdf9eTTf/NiAF/z4qEOZdTTdJ////AHBhTgAAAAAAAAAAAAAA + AACBc2MAvreuA0UxGKM1IAX/cWJP/5qQgv9pWUX/Wkgy/0g1HP9/cWD//Pv7////////////+fj4/5SJ + ev9GMhn/OyYM/zwnDf89KA7/PSkO/0MvFv90ZlP/lYp8/z4qD/84Iwj/RTEYo764sAOBdGQAAAAAAAAA + AAAAAAAAAAAAAAAAAAA6JQoASzggXDkkCfxDLxb/bF1J/4ByYf+NgXL/lop8/62kmf/g3dn///////j3 + 9v/f29f/2NTP/8/KxP/CvLT/pZyQ/5WKfP+Wi33/mo+C/4l9bf9HNBv/NyIG/zolCvxLOCBcOiUKAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAF1MNgBdTDYkQCwS4jciB/81HwT/NR8E/zciB/88Jw3/QS0T/1pI + Mv+UiHr/cGBN/0s4H/9KNx//cGBN/5aLff9fTzn/RzMa/0c0G/9BLRT/NyIH/zciBv84Iwj/QCwS4l1M + NiRdTDYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAi39wAI2AcgVWRC1lSDUcqEMvFs0/KxHqOiUK/Dgj + CP84Igf/NyEG/zUgBf81IAT/NyIH/zciB/81IAX/NSAF/zYhBv83Igf/NyIH/zolCvw+KhDqQS0UzUYy + GahUQiplkIR1BYyAcQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJuRhACzq6IDeGpYEWNT + PipCLhV5OiUK8jgjCP84Iwj/OCMI/zgjCP84Igf/OCIH/zgjCP84Iwj/OCMI/zgjCP85JAryQi4UeWFQ + OypvX0wRpJqOA4x/cQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAASzgfAFVDKwRCLhRtOiYL8jgjCP84Iwj/OiUL+0EsE+dBLRPnOyYL+zgjCP84Iwj/OiUL8kEt + E21RPicESTUdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAU0EqAGBPOgRFMhl9QS0T7Ek2HblNOiJpZFQ/MGhYQzBRPidpSzkguUIu + FOxEMRd9XEw2BFE/KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+B///wAA//4A + AH/8AAA//AAAP/AAAA/AAAADwAAAA8AAAAPAAAADwAAAA4AAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AACAAAABAAAAAAAAAAAAAAAAAAAAAIAAAAHgAAAH4AAAB/AAAA/wAAAP8AAAD/wAAD//gAH//8AD/ygA + AAAwAAAAYAAAAAEAIAAAAAAAACQAABMLAAATCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGxc + SACShXcEaVlFT0o3HstINBzLZFM+T4R3ZwRkUz4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AACXjH4AAAAAAGdXQyNRPiaTQCsR6zkkCf85JAn/PyoQ6048JJR+cF8v////AIl8bAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1c/OAGtcSAB0ZVMib2BMU4F0 + Y1OMgHBJSTYdIT4pDx1rW0cmXUw2dEIuFNY6JQr+OCMH/zgjCP84Iwj/OCMI/zolC/9HMxrgW0kzdWtb + RyY+KQ8dSTYdIYyAcEmBdGNTb2BMU3RlUyJrXEgAysjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfW9eAIR3 + Zw1MOSGsQS0T/0UxGP5FMRj4OSQK4zolC+FBLRPmPSkO/DgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP83Igf/PSgO/EEtE+Y6JQvhOSQK40UxGPhFMRj+QS0T/0s5IKyEd2YNfW9dAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AACgl4sAAAAAAGFQOmw9KA76NyIH/zciB/83Igf/OCMI/zgjCP84Iwf/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjB/84Iwj/OCMI/zciB/83Igf/NyIH/z0oDvpgTzpsAAAAAKKY + jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAABXRS4Aa1tHJEQxF9U4Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zYhBf82IQX/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP9EMBfValpGJFZELQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAKCWiQDEvbUHVEIrmjolCv84Iwj/NiEF/zUgBP81IAT/NSAE/zUg + BP83Igf/OCMI/zgjCP82IQb/OSQJ/2BPOf9gUDr/OSQJ/zUgBP81IAT/NSAE/zUgBP81IAT/NSAE/zUg + BP81IAT/NiEG/zgjCP86JQr/VEEqmsG6sweek4cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAl41+AODd2ARtXUleQi4U9DciB/85JAn/YE86/31v + Xf98b13/fG9d/3lrWf9KNx//NB8D/zYgBf9PPSX/n5WI/+zq5//t6+j/oZeK/21dSv94aVf/fG9d/3xv + Xf98b13/fG9d/3ttXP9wYU7/VUMs/zomC/82IQb/Qi4U9GxdSV7h3dkEl41+AAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AGtaRgCNgHAFY1I9OlNBKp9EMBbvOCMI/zgj + CP8+Kg//wbuy/7Wto/+il4v/oZeL/8nEvf+so5j/alpG/4l9bf/b19L//v79////////////7Orn/763 + rv+onpP/opiM/6OYjP+jmIz/opiM/6Sajv+wp53/w721/6ifk/9OOyP/NiEG/0QwFu9TQiqfY1M+Oo2A + cQVrW0cA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wANAAAAqKCUDlxKND9JNh2fQCwS7Tkk + Cf83Igf/OCMI/zgjCP8/KxH/wLqx/11MNv81IAT/NR8E/4+DdP/+/v7/+Pf2//v7+/////////////// + ///Qy8T/Y1I9/z0pD/84Iwj/NyIH/zciB/83Igf/NyIH/zciB/85JAn/TToh/5qPgv+2rqT/RzQb/zYh + Bv85JAn/QCwS7Uc0G59ZRzE/p52RDh4IAAD08e8AAAAAAAAAAAAAAAAAAAAAAJ+WiQCjmo0YX085sj8r + EfM4Iwj/OCMH/zgjCP84Iwj/OCMI/zgjCP8/KxH/wLqx/15NN/82IAX/NSAF/4+DdP////////////// + /////////////+zq5/9jUj3/NB8D/zgjCP84Iwj/OCIH/zgjB/84Iwf/OCMH/zgjCP84Iwj/NiEG/zsm + C/+upZr/in5u/zYgBf84Iwj/OCMH/zgjCP8/KxHzXkw3sqGXihiflIcAAAAAAAAAAAAAAAAAAAAAAIF0 + YwB/cmEtSDUc7TchBv84Iwj/OCMI/zgjCP84Iwj/OCMI/zciB/89KQ7/wLmw/15NN/82IAX/NSAF/4+D + dP///////////////////////////8C6sf88Jw3/OCMI/zkkCf8+KhD/PysR/z8rEf8/KxH/PysR/zsm + DP83Igb/OCMI/zUgBP9sXEj/tKyi/zwoDv84Iwj/OCMI/zgjCP83IQb/SDUc7YByYS2CdGQAAAAAAAAA + AAAAAAAAAAAAAFZFLgBWRC0tPysR7TgjCP84Iwj/OCMI/zgjCP83Igf/NSAE/zwoDf9rXEj/1NDK/11M + Nv82IAX/NSAF/4+DdP///////////////////////////6KYi/82IQb/NyIG/0s4IP+6s6n/y8W+/8rE + vP/KxLz/ysS9/7uzqv9hUDv/NyEG/zYhBv9RPyj/vLSr/0g0HP83Igf/OCMI/zgjCP84Iwj/PysR7VZE + LS1XRS4AAAAAAAAAAAAAAAAAAAAAAEEtEwBINBsxOSUK7zgjCP84Iwj/OCMI/zgjB/9ALBL/cGFO/7ix + p//y8O7/8fDu/1tKM/82IAX/NSAF/4+DdP///////////////////////////5aKfP8yHAH/Mx0C/089 + Jf/p5+T///////////////////////////+xqZ7/OSQK/zciBv9KNx//vreu/1NBKv83IQb/OCMI/zgj + CP84Iwj/OSUK70c0GzFBLRMAAAAAAAAAAAAAAAAAAAAAAGVUPwB+cF5eQCwS/jgiB/84Iwj/OCMI/zYg + Bf+GeWj/9/b1////////////8fDu/1tKM/82IAX/NSAF/4+DdP///////////////////////////7ix + p/93aFb/d2lX/4p+bv/x7+3///////////////////////////+/ubD/PCgN/zciBv9INBv/1tHM/4p9 + bf81IAX/OCMI/zgjCP84Igf/QCwS/n5wXl5lVD8AAAAAAAAAAAAAAAAAAAAAAFpJMwBsXEhpPysR/zgj + CP84Iwj/OCMI/zYhBv+glYj/////////////////8fDu/1tKM/82IAX/NSAF/4+DdP////////////// + //////////////7+/v/+/f3//v79//7+/v////////////////////////////////+/uLD/PCgN/zci + Bv9HNBv/3NjT/6GYi/82IQb/OCMI/zgjCP84Iwf/PysR/2xdSWlbSTMAAAAAAAAAAAAAAAAAn5WIAAAA + AABPPSV9OiYL/zgjCP84Iwj/OCMI/zgjCP+tpZr/////////////////8fDu/1tKM/82IAX/NSAF/4+D + dP////////////////////////////////////////////////////////////////////////////// + //+/uLD/PCgN/zciBv9HNBv/3NjT/6+nnP84Iwj/OCMI/zgjCP84Iwj/OiYL/089JX0AAAAAn5SIAAAA + AAD///8AY1M+AG5fS0BFMRjnNyIH/zgjCP84Iwj/OCMI/zkkCf+8taz/////////////////8fDu/1tK + M/82IAX/NSAF/4+DdP////////////////////////////////////////////////////////////// + //////////////////+/uLD/PCgN/zciBv9HNBv/3dnU/764r/85JAn/OCMI/zgjCP84Iwj/NyIH/0Ux + GOdsXUlAYlE8AP///wB7bVsAgHJhH0s4IMY4Iwj/OCMI/zgjCP84Iwj/NyIH/0MvFf/OycP///////// + ////////8fDu/1tKM/82IAX/NSAF/4+DdP////////////////////////////////////////////// + //////////////////////////////////+/ubD/PCgN/zciBv9HNBv/3NjT/9HMxf9DLxX/NyIH/zgj + CP84Iwj/OCMI/zgjCP9LOCDGf3JgH3ttWwCimYwNUkAomjolC/84Iwj/OCMI/zgjCP84Iwj/OCMI/5CE + df/6+vn/////////////////8fDu/1tKM/82IAX/NSAF/4+DdP////////////////////////////// + //////////////////////////////////////////////////++t67/PCcN/zciBv9HNBv/2tbQ//38 + /P+RhXb/OCMI/zgjCP84Iwj/OCMI/zgjCP86JQv/UT8omqCWiQ1ZSDJ6PioQ9jgjB/84Iwj/OCMI/zgj + CP82IAX/bl9L/+3r6f//////////////////////8fDu/1tKM/82IAX/NSAF/4+DdP////////////// + //////////////////////////////////////////////////////////////////+qoZX/OCMI/zci + B/9HNBv/2dXQ///////u7Or/cWJO/zYgBf84Iwj/OCMI/zgjCP84Iwf/PioP9llHMXpKNx/tNyIH/zgj + CP84Iwj/OCMI/zYhBv9UQiv/19LN////////////////////////////8fDu/1tKM/82IAX/NSAF/4+D + dP////////////////////////////////////////////7+/v/5+Pj/+fj3//n49//5+Pf/9/b1/9PO + yP9fTjj/NiEG/zUgBP9PPCT/4t/c////////////2NTP/1RCK/82IQb/OCMI/zgjCP84Iwj/NyIH/0s3 + H+1XRS+1OiUK/zgjCP84Iwj/OCMI/zUgBP9uX0z/9/b1////////////////////////////8fDu/1tK + M/82IAX/NSAF/4+DdP///////////////////////////////////////////+Th3v91Z1T/aFhE/2lZ + Rf9pWUX/ZlVB/0o3Hv83Igf/OyYL/0s4IP+flYj//Pv7////////////9/b1/25fTP81IAT/OCMI/zgj + CP84Iwj/OiUK/1dGL7VrW0hMQCsS7zgjB/84Iwj/OCMI/zciB/9FMRj/zsnC//////////////////// + ////////8fDu/1tKM/82IAX/NSAF/4+DdP///////////////////////////////////////////9vX + 0v9GMhn/NB4D/zUgBP81IAT/NSAE/zciB/82IAX/a1tH/9/c1//8+/v/////////////////zsnC/0Ux + GP83Igf/OCMI/zgjCP84Iwf/QCsR72tbR0yglooMUj8ouTgjCP84Iwj/OCMI/zgjCP81IAX/iX1t//7+ + /f//////////////////////8fDu/1tKM/82IAX/NSAF/4+DdP////////////////////////////// + /////////////9vX0v9GMhn/NB4D/zUgBP81IAT/NSAE/zciB/82IAX/a1tH/9/c2P/8+/v///////// + ///+/f3/iX1t/zUgBf84Iwj/OCMI/zgjCP84Iwj/Uj8ouaGXiwwbAwAAZVVAYD0pD/o4Iwj/OCMI/zgj + CP82IQb/UD4m/+Dd2P//////////////////////8fDu/1tKM/82IAX/NSAF/4+DdP////////////// + /////////////////////////////+Th3v91Z1T/aFhE/2lZRf9pWUX/ZlVB/0k2Hv83Igf/OyYL/0s4 + IP+glon//Pv7///////g3Nj/UD4m/zYhBv84Iwj/OCMI/zgjCP89KQ/6ZVVAYBsDAABtXUoAcWNPE0Et + FMY4Iwj/OCMI/zgjCP84Iwf/PSkP/8S+tv//////////////////////8fDu/1tKM/82IAX/NSAF/4+D + dP////////////////////////////////////////////7+/v/5+Pj/+fj3//n49//5+Pf/9/b1/9LO + yP9eTjj/NiEG/zUgBP9PPCT/4+Dc///////Evrb/PSkP/zgjCP84Iwj/OCMI/zgjCP9BLRTGcWJQE21d + SgCwp5wAxb63C1I/KL83Igf/OCMI/zgjCP83Igf/RzMa/9nW0P//////////////////////8fDu/1tK + M/82IAX/NSAF/4+DdP////////////////////////////////////////////////////////////// + //////////////////+qoZX/OCMI/zciB/9HNBv/2dXQ///////Z1dD/RjMa/zciB/84Iwj/OCMI/zci + B/9SQCi/xsG4C7CpngByYk8AdmdVFUUxGNU4Iwj/OCMI/zgjCP82IQb/WEYw/+vp5v////////////// + ////////8fDu/1tKM/82IAX/NSAF/4+DdP////////////////////////////////////////////// + //////////////////////////////////++t6//PCcN/zciBv9HNBv/2dXQ///////r6eb/WEYv/zYh + Bv84Iwj/OCMI/zgjCP9FMhjVdmhVFXFjUAB+cF4Af3FgNkUxGPA3Igf/OCMI/zgjCP81IAT/b2BN//n5 + +P//////////////////////8fDu/1tKM/82IAX/NSAF/4+DdP////////////////////////////// + //////////////////////////////////////////////////+/ubD/PCgN/zciBv9HNBv/2dXQ//// + ///5+fj/b2BN/zUgBP84Iwj/OCMI/zciB/9FMRjwf3JgNn5wXwBKNh4AXUw2Sj0oDvk4Iwj/OCMI/zgj + CP82IAX/bF1J/+7s6v//////////////////////8fDu/1tKM/82IAX/NSAF/4+DdP////////////// + //////////////////////////////////////////////////////////////////+/uLD/PCgN/zci + Bv9HNBv/2dXQ///////v7ev/bV1J/zYgBf84Iwj/OCMI/zgjCP89KA75XUw2Sko3HgA6JgsAY1M+ejwn + Df84Iwj/OCMI/zgjCP84Iwj/OiUL/3lqWP/j4N3/////////////////8fDu/1tKM/82IAX/NSAF/4+D + dP////////////////////////////////////////////////////////////////////////////// + //+/uLD/PCgN/zciBv9HNBv/2tbR/+bk4P96bFr/OyYL/zgjCP84Iwj/OCMI/zgjCP88Jw3/ZFM+ejsm + CwAAAAAATz0ljDkkCf84Iwj/OCMI/zgjCP84Iwj/OCMI/zYhBv9hUDr/zsnC//7+/v//////8fDu/1tK + M/82IAX/NSAF/4+DdP////////////////////////////7+/v/+/f3//v79//7+/v////////////// + //////////////////+/uLD/PCgN/zciBv9JNR3/x8G5/21dSf82IAX/OCMI/zgjCP84Iwj/OCMI/zgj + CP85JAn/Tz0ljAAAAAAAAAAAcGFOREo2HtQ4Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP83Igf/emxa//Dv + 7f//////8fDu/1tKM/82IAX/NSAF/4+DdP///////////////////////////7ixp/93aFb/d2lX/4p+ + bv/x7+3///////////////////////////+/ubD/PCgN/zciBv9JNh3/urKo/047I/83IQb/OCMI/zgj + CP84Iwj/OCMI/zgjCP9JNh3Ubl9LRAAAAADU0MsAAAAAAGFRPCROOyO3OiYL/TgjB/84Iwj/OCMI/zci + B/9KNh7/x8G5/8zGv//X087/2tbR/1hHMP82IQX/NSAF/4+DdP///////////////////////////5SJ + e/8yHAH/Mx0C/089Jf/p5+T///////////////////////////+xqZ7/OSQK/zciBv9LNx//u7Oq/088 + JP83Igb/OCMI/zgjCP84Iwf/OiYL/U47I7dhUTwkAAAAANTQywAAAAAAxL61AEUxGQCAcmEUSzggiz8r + Efo4Iwj/OCMI/zciBv9PPSX/u7Oq/048JP9EMRf/UD4m/z4qEP84Iwj/NSAF/4+DdP////////////// + /////////////52Thv82IQb/NyIG/0s4IP+6s6n/y8W+/8rEvP/KxLz/ysS9/7uzqv9hUDv/NyEG/zYh + Bv9RPyj/vLWs/0g1HP83Igf/OCMI/zgjCP8/KxH6Sjcfi31vXRRDLhUAxL61AAAAAAAAAAAAAAAAAAAA + AACFeGgAnJOFCmRUP3w+KhDzOCMI/zciBv9PPCX/urKp/0k2Hf82IQb/NiEG/zgjCP84Iwj/NSAF/4+D + dP///////////////////////////7qyqf86JQr/OCMI/zkkCf8+KhD/PysR/z8rEf8/KxH/PysR/zsm + DP83Igb/OCMI/zUgBP9rXEj/tK2i/zwoDv84Iwj/OCMI/z4qEPNjUz58mY+BCoJ2ZQAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAjIBwAJyShAhJNh61OCMI/zciBv9PPSX/urKp/0c0G/80HgP/NSAF/zYh + Bv83Igf/NSAE/4+DdP///////////////////////////+Xi3/9ZSDH/NB8E/zgjCP84Iwj/OCIH/zgj + B/84Iwf/OCMH/zgjCP84Iwj/NiEG/zsmC/+tpZr/in5u/zYgBf84Iwj/OCMI/0o3HrWckoUIjIBwAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnJGEACgRAABUQituOyYL/zciB/9KNh7/x8G5/6GX + iv+BdGP/bV5K/1hHMP9KNx7/OycM/5CEdf/////////////////////////////////Fv7f/XUw2/zwo + Df83Igf/NyIH/zciB/83Igf/NyIH/zciB/85JAn/TDoh/5qPgf+2rqT/RzQb/zciB/84Iwj/OyYL/1RC + K24oEQAAnJKGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF1MNgBiUjwtQCsR6Dci + B/86JQr/ZVVA/4p+bv+imIz/same/722rf+/uK//uLGn/9XRy//5+Pj///////////////////////z8 + +//9/fz/7evo/8bAuP+so5j/p52S/6Sajv+imIz/opiM/6Sajv+wp5z/w721/6ifk/9OOyP/NiEG/zgj + CP84Iwj/QCsR6GJSPC1dTDYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHhq + VwCNgnIIRTIYszgjCP84Iwj/NiAF/zUgBP82IQb/OyYM/0MwFv9QPSb/ZFQ//3FiT/9+cF//4d3Z//f2 + 9f/c2NP/saie/31vXf98b13/rqWa/9vY0//5+Pf/4+Hd/4x/cP97blz/fG9d/3ttXP9wYU7/VUMs/zom + C/83Igb/OCMI/zgjCP84Iwj/RTIZs46Dcwh4a1gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAMG5sAAvGQAAXEo0ejsmDP83Igf/NyIH/zgjCP84Iwj/OCMI/zciB/83IQb/NSAF/zUg + BP82IQX/YE85/21eSv9JNh3/OSQJ/zUgBP81IAT/OCMI/0g0HP9rW0f/YE86/zYgBf81HwT/NSAE/zUg + BP81IAT/NiEG/zgjCP84Iwj/NyIH/zciB/87Jgz/XEs1ei4YAADEvrQAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYRjAAbl5LLFNBKrZEMRfaQi4U9DwnDfw5JQr/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/NiEF/zUgBf83Igf/OCMI/zgjCP84Iwj/OCMI/zciB/81IAX/NiEF/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zkkCf87Jgz8QCwS9EIuFdpRPie2bl9MLFhHMQAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARTEYALGpngp6bFoaa1xIO2JS + PWRRPyiMTjsjvzolC/A4Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP86JQrwTjsjv088JIxdSzVkZFM+O3FiTxqil4sKNCAGAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAPb19AD///8BnpOGCEIuFWY7JgzwOCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zsmC/BBLRRmnZOGCP///wH29fQAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVkMrAG5dSQJGMxlmPCcN7zgjCP84Iwj/OCMI/zgj + CP84Iwf/OCMI/zsmC/87Jgv/OCMI/zciB/84Iwj/OCMI/zgjCP84Iwj/PCcM70QwF2ZmVkMCUT8oAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFlHMABxYU4CSTYdZT0o + Du84Igf/OCMJ/zwnDP1AKxHmSTYdvV1MNoleTTiJTDkhvUEtE+Y8KA39OCQJ/zciB/88KA3vSDUcZWlb + RgJUQywAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AABjUj0Af3FiA007InRFMhjvTjsjw11NN3xlVUAyi39vC////wH///8BlIl7C21dSTJjUz58UT8ow0cz + Gu9MOCB0eGpWA19ONwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAD///gf//8AAP//8A///wAA//AAAA//AAD/4AAAB/8AAP/gAAAH/wAA/8AAAAP/ + AAD/gAAAAf8AAP8AAAAA/wAA/AAAAAA/AADwAAAAAA8AAOAAAAAABwAA4AAAAAAHAADgAAAAAAcAAOAA + AAAABwAA4AAAAAAHAADgAAAAAAcAAOAAAAAABwAAwAAAAAADAACAAAAAAAEAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAQAAgAAAAAABAACAAAAAAAEAAIAA + AAAAAQAAgAAAAAABAACAAAAAAAEAAIAAAAAAAQAAgAAAAAABAACAAAAAAAEAAMAAAAAAAwAA4AAAAAAH + AADwAAAAAA8AAPgAAAAAHwAA/AAAAAA/AAD8AAAAAD8AAPwAAAAAPwAA/gAAAAB/AAD+AAAAAH8AAP8A + AAAA/wAA//AAAA//AAD//AAAP/8AAP/+AAB//wAA//8AAP//AAAoAAAAoAAAAEABAAABACAAAAAAAACQ + AQATCwAAEwsAAAAAAAAAAAAA////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AHNjUf9zY1H/////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8Am5CD/z4pD/84Iwj/OCMI/zsnDP+NgXL/////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wDCvLT/UD0m/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/0w4IP+2raP/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wBvXkv/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/2dX + RP////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AI2Bcv87Jgv/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OSQJ/4FzYv////8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8Atq2j/0k1Hf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/RjMZ/6eek/////8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AY1Q//zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/X085/9DMxv////8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wCNgXL/OSQJ/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/gXNi/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AKee + k/9DLxf/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9AKxL/m5CD//// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8ApJuQ/3ttW/+Ie2r/kIN0/5eMfv+il4z/q6OW/7Ssov+9tq3/yMK6/9DMxv////8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8AysS+/1hHMP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9YRzD/ycS9/////wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wDQzMb/yMK6/722rf+0rKL/q6OW/6KXjP+XjH7/kIN0/4h7 + av97bVv/pJuQ/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////ADolCv84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9CLhT/Sjcf/1NA + KP9fTzn/Xk02/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9cSzX/X085/1NAKP9KNx//Qi4U/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zolCv////8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AGJRPf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/X085/////wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AKmg + lP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP+poJT/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wBDLxf/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/Qi4U//// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wB6alj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP96bFr/////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDHwrn/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/8O9tf////8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8AU0Ao/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP9QPSb/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8AlYp9/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/5eMfv////8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////ADwoDf84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP87Jgv/////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AGpbRv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/2ZWQv////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////ALKq + oP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/sqqg//// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wBINRz/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/0YzGf////8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wCDd2X/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OSQJ/1dGLv/Iwrr/yMK6/1dGLv84Iwj/OSQJ/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/gnRk/////wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDOyML/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/QCsS/5uQg/////////////// + ////////p56T/0IuFP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP/MxsD/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8AWEcw/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/emxa////////////////////////////////////////////iHtq/zkk + Cf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/VUQt/////wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8An5aI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/2ZW + Qv96bFr/emxa/3psWv96bFr/emxa/3psWv96bFr/emxa/3psWv96bFr/emxa/3psWv96bFr/dGRT/0Yz + Gf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9eTTb/0MzG//// + ///////////////////////////////////////////////////QzMb/Xk02/zgjCP84Iwj/Qi4U/1xL + Nf9pWUX/dGRT/3psWv96bFr/emxa/3psWv96bFr/emxa/3psWv96bFr/emxa/3psWv96bFr/emxa/3ps + Wv96bFr/emxa/3psWv9uYEz/ZlVB/048JP86JQr/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+floj/////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8Av7ev/z8qEP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/6adkf////////////////////////////// + ////////////////////////////////////////////////////////RjMZ/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/0YzGf+tpJj///////////////////////////////////////// + ///////////////////////////////////KxL7///////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////8/Lxf+hl4n/Y1M+/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/PyoQ/7+3r/////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wC0rKL/V0Yu/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP////////////////////////////////////////////////////////////// + /////////////////////////////3xuXf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OiUK/4l+ + bv////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////iX5u/zsnDP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/V0Yu/7Ss + ov////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AKeek/9OPCT/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj///////// + ////////opeM/6KXjP+il4z/opeM/6KXjP+il4z/opeM/6KXjP+il4z/opeM/6KXjP/JxL3///////// + //+Ie2r/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/ZlZC//////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////8O9 + tf+2raP/qJ2S/6KXjP+il4z/opeM/6KXjP+il4z/opeM/6KXjP+il4z/opeM/6KXjP+il4z/opeM/6KX + jP+il4z/opeM/6Sajv+yqZ//wLmx/87Iwv//////////////////////////////////////altG/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/Tjwk/6eek/////8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8Am5CD/0c0G/84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy///////////////////////Szsj/ycS9/722 + rf+2raP/wryz//////////////////////////////////////////////////////////////////// + /////////////////////////////7Cnnf92Z1X/RjMZ/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/1VDLP+Qg3T///////////////////////////+ajoH/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/RzQb/52RhP////8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wCQg3T/QS0T/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////pp2R/0Yz + Gf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP9tX0r//////////////////////6uhlf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/QS0T/5CEdf////8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AI2Bcv8+KQ//OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj///////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////ZlVB/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zsnDP+kmo7///////// + ////////lIh5/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/PCgN/4FzYv////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AgXNi/zol + Cv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////VUMs/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/42Bcv////////////////9mVUH/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OSQJ/3Nl + Uv////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AMnEvf9sXEj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////ZlZC/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/qJ2S/////////////////zolCv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/21dSf/KxL7/////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8Awry0/2NT + Pv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj///////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + ////////////////////////pp2R/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/z0p + Dv////////////////9/cmD/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/1xLNf+2raP/////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8AubOq/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////0c0 + G/84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/dGZU/////////////////zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/7mxqP////8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AKmglP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + /////////////////////////////////////////////6uhlf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP////////////////9eTTb/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP+poJT/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wCZkIL/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj///////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////9lVUD/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/k4d3////////////m5CD/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/m5CD/////wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8AjIBx/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + ////////////////////////////////////////////////////////////////////////PCgN/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/1VELf///////////9HM + x/85JAn/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/4yAcf////8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AHtuXP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + ////////////////////////////////////////0MzG/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/////////////////OSQJ/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP9/cmD/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wBvXkv/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj///////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + /////////////6adkf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/PCgN/21d + Sf9tXUn/bV1J/21dSf9tXUn/bV1J/21dSf9tXUn/bV1J/21dSf9tXUn/bV1J/21dSf9tXUn/bV1J/21d + Sf9tXUn/YlI8/0MvF/84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/8S+t////////////1dGLv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/b15L/////wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8AX085/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/SDUc/////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////+PgnT/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/0g1HP////////////////////////////// + ////////////////////////////////////////////////////////////////////////wbux/1E+ + J/85JAn/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+onZL///////// + //9wYU3/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/19POf////8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AFA+Jv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP9QPSb/q6OW//////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + ////////////////////////////////////////gnRk/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP9INRz///////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////U0Ao/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/mo6B////////////hXdn/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP9QPib/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wBCLhT/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/1pIMf+2raP///////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + /////////////3ZnVf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/SDUc//// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////8jCuv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv///////////5CEdf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/Qi4U/////wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8AOCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/X043/7ato////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////9pWkX/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/0g1HP////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////UT4n/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+Bc2L///////// + //+YjYD/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP////8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////ADgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9wYU3/zsjC//////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + ////////////////////////////////////////aFdD/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP9INRz///////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////3doVf84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV////////////nZGE/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wA4Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OiUK/3xuXf////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + /////////////2hXQ/84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/SDUc//// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////+DeGb/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf///////////7KqoP86JQr/OSQJ/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/////wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wDPycL/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/RjMZ//////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////9oV0P/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/0g1HP////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////hXdn/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + /////////////0UxGP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP/PycL/////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8AwLmx/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/1tKNf////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + ////////////////////////////////////////aFdD/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP9INRz///////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////4V3Z/84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV//////////////////////9bSTT/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/wLmx/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////ALKpn/84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP9qW0b///////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////+Fd2f/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf//////////////////////aVpF/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/7KqoP////8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wCimY3/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/empY//////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////hXdn/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + /////////////3doVf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+kmo7/////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8AlIh5/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/4d7a/////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////4V3Z/84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV//////////////////////+GeWj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/lIp7/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AIV3Z/84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP+XjH7///////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////+Fd2f/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf//////////////////////lot8/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/4Z5aP////8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wB1aFT/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/pp2R//////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////hXdn/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + /////////////6SbkP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX/////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8AZ1dE/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/7Ssov////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////4V3Z/84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV//////////////////////+yqqD/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/Z1dE/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AEYzGf84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP/CvLT///////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////+Fd2f/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf//////////////////////wry0/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/0YzGf////8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AG9eS/84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/0czH//////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////hXdn/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + /////////////9DMxv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/altG/////wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AJuQ + g/84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI//////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////4V3Z/84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV////////////////////////////OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP+bkIP/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AMzGwP86JQr/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////+Fd2f/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf///////////////////////////zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OSQJ/8jC + uv////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wBMOCD/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP85JAn///////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////hXdn/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + //////////////////85JAn/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9JNR3/////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wBsXEj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/RTEY//////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////4V3Z/84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV////////////////////////////QzAW/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/2lZRf////8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wCXjH7/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/1RC + Kv////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////+Fd2f/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf///////////////////////////1NAKP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/mI2A/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wDGwLj/OSQJ/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+elIb///////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////hXdn/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + //////////////////+elIb/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zkkCf/GwLj/////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8ARzQb/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zkkCf9sXEj///////////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////4V3Z/84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV/////////////////////////////////25g + TP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/RjMZ/////wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8AZlZC/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9KNx////////// + //////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////+Fd2f/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf//////////////////////////////////////TToi/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP9iUT3/////AP///wD///8A////AP///wD///8A////AP///wD///8Ak4d3/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP87Jwz/zsjC//////////////////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////g3dl/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + /////////////////////////////9POyP88KA3/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/5KH + eP////8A////AP///wD///8A////AP///wD///8Aw762/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/opmN//////////////////////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////3doVf84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV//////////////////////////////////// + ////////opeM/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/wLmx/////wD///8A////AP// + /wD///8A////AEYzGf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/cWJP//////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////9iUjz/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf////////////////////////////////////////////////92Z1X/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/0UxGP////8A////AP///wD///8A////AGNUP/84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/Tjwk//////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////PCgN/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + /////////////////////////////////////////////1E+J/84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/YVA7/////wD///8A////AI2Bcv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OycM/9DM + xv////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////ubGo/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV//////////////////////////////////// + ////////////////////////PCgN/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+PgnT/////AMK8 + tP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/6adkf////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////1pIMf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3tuXP///////////////////////////////////////////////////////////6ad + kf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/7+3r/9TQCj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////4l8 + bP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+QhHX///////// + ////////////////////////////////////////////////////////d2hV/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP9TQCj/lYp9/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/1A9Jv////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////3tuXP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/wryz//////////////////////////////////// + //////////////////////////////////9TQCj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/l4x+//// + /wA7Jwz/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //+5s6r/hnlo/0ArEv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/UT4n//////////////////////////////////////////////////////////////////// + /////////////zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OyYL/////wD///8Aemxa/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/w721//////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////8O9tf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OiUK/8nEvf////////////// + /////////////////////////////////////////////////////////////8K8tP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3ttW/////8A////AM7Iwv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/29eS/////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ///////////////////DvbX/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/QCsS/7u1rP////////////////////////////////////////////// + //////////////////////////////////9tX0r/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP/Py8X/////AP// + /wD///8AYU86/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj///////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////w721/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9HNBv/hnlo//// + //////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9eTTb/////AP///wD///8A////ALSsov84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/4p+cP////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////8O9tf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/XEs1/722rf////////////////////////////////////////////// + ////////////////////////////////////////////////////////iX5u/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/tKyi/////wD///8A////AP///wD///8ASTYe/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP8/KhD///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ///////////////////DvbX/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/3xu + Xf////////////////////////////////////////////////////////////////////////////// + /////////////////////////////0EtE/84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/SDUc/////wD///8A////AP// + /wD///8A////AJiNgP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/6adkf////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////w721/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP98bl3///////////////////////// + /////////////////////////////////////////////////////////////////////////////6Sb + kP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/5qOgf////8A////AP///wD///8A////AP///wD///8AOycM/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP9QPib///////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////8O9tf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/fG5d//////////////////////////////////////////////////// + //////////////////////////////////////////////////9TQCj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zwo + Df////8A////AP///wD///8A////AP///wD///8A////AHxuXf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/8G7 + sf////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ///////////////////DvbX/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/3xu + Xf////////////////////////////////////////////////////////////////////////////// + ///////////////////AubH/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP98bl3/////AP///wD///8A////AP// + /wD///8A////AP///wDSzsj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9sXEj///////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////w721/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9cSzX/vbat//////////////////// + ////////////////////////////////////////////////////////////////////////aVpF/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/087I/////wD///8A////AP///wD///8A////AP///wD///8A////AGJS + PP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI//////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////8O9tf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/SDUc/4d7a/////////////////////////////// + /////////////////////////////////////////////zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/X085//// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wC2r6T/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP+He2v///////////////////////////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ///////////////////DvbX/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/QS0T/722rf////////////////////////////////////////////// + /////////////4Z6af84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/7avpP////8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AEo3H/84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/QCsS//////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////w721/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP87Jwz/ysS+//////////////////////////////////////////////////////9AKxL/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/0k2Hv////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wCbkIP/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+kmo7///////////////////////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////ubGo/4J0ZP8/KhD/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/1NAKP////////////// + //////////////////////////////////+il4z/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+bkIP/////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////ADwoDf84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/bmBM//////////////////////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////3RmVP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/wry0//////////////////////////////////// + ////////bmBM/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP88KA3/////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wB/cmD/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/42Bcv////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////////////////////g3hm/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/5GGdv///////////////////////////////////////////4yAcf84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/f3Fh/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////ADgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+vp5z///////////////////////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //9XRi7/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP97blz///////// + //////////////////////////////////+tpJj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/////wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wA4Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/0MzG//////////////////////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////ubOq/zkkCf84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV//////////////////////////////////// + ////////zsjC/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP////8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wDMxsD/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OSQJ//////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////89KQ7/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf////////////////////////////////////////////////84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/zcjB/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8Aq6GV/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/0w4IP////////////////////////////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////Y1Q//zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + ////////////////////////////////////////Sjcf/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/6ujlv////8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AIl8bP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP9tX0r///////////////////////////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////3doVf84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV//////////////////////////////////// + /////////////2xcSP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+Jfm7/////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wBoV0P/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/joRz//////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////+DeGb/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf////////////////////////////////////////////////+NgXL/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/Z1dE/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8ARzQb/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/7Cnnf////////////////////////////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////hXdn/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + ////////////////////////////////////////r6ec/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/0YzGf////8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////ADgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP/RzMf///////////////////////////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////4V3Z/84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV//////////////////////////////////// + /////////////9DMxv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AMrEvv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj///////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////+Fd2f/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf//////////////////////////////////////////////////////OSQJ/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/8rGv/////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wCpoJT/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/TToi//////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////hXdn/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + /////////////////////////////////////////////0w4IP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+poJT/////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AiHtq/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/29e + S/////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////4V3Z/84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV//////////////////////////////////// + //////////////////9tX0r/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/h3tr/////wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AGZVQf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+Qg3T///////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////+Fd2f/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf//////////////////////////////////////////////////////joRz/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/2ZWQv////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wBGMxn/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/qJ2S//////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////hXdn/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + /////////////////////////////////////////////6idkv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9GMxn/////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AOCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/z0p + Dv+2r6T///////////////////////////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////4V3Z/84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV//////////////////////////////////// + /////////////722rf9AKxL/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/////wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8AycS9/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/5SKe/////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////+Fd2f/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf///////////////////////////////////////////6KXjP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP/KxL7/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AKee + k/84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/cGFN//////////////////////////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////hXdn/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + /////////////////////////////3VoVP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/qaCU//// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wCGemn/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP9UQir///////////////////////////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////4V3Z/84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV/////////////////////////////////1hH + MP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/4h7av////8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8AZVVA/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/0Iu + FP/CvLP///////////////////////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////+Fd2f/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf//////////////////////x8C4/0UxGP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP9mVUH/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AEUx + GP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OiUK/6KXjP////////////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////hXdn/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + ////////p56T/zolCv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/QzAW//// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wA4Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/fnFf//////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////4V3Z/84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV////////////q6OW/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP////8A////AP///wD///8A////AP// + /wD///8A////AP///wDIwrr/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP9fTjf///////////////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////+Fd2f/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf///////////52RhP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/yMK6/////wD///8A////AP///wD///8A////AP///wD///8A////AHxu + Xf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/0k2 + Hv/Py8X//////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////hXdn/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + //+dkYT/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/fG5d//// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8Ap56T/zolCv84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/Tjwk//////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + ////////////////////////////////////////Z1dE/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP9INRz///////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////4V3Z/84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV////////////nZGE/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/m5CD/////wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wDEvrf/Qy8X/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP9RPif/hnlo/3xuXf////////////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + /////////////2VVQP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/SDUc//// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////+Fd2f/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/3doVf///////////52RhP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP9DLxf/xL63/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wBVRC3/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9QPSb///////// + /////////////////////////////////////////////////////////////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////9nV0T/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/0g1HP////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////g3hm/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP93aFX///////// + //+dkYT/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9VRC3/////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AHNj + Uf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + ////////////////////////////////////////ZVVA/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP9INRz///////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////3doVf84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/d2hV////////////nZGE/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP9zY1H/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8Alot8/zgjCP85JAn/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/52RhP///////////8G7sf+7s6v///////////////////////////////////////// + ////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + /////////////2dXRP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/SDUc//// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////9QPSb/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/4FzYv///////////5iNgP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+Wi3z/////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wC7s6v/PyoQ/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+dkYT///////// + //93aFX/OCMI/zgjCP84Iwj/RjMZ/2JSPP98bl3/mI2A/7Ssov/OyML//////zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////9pWUX/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/0g1HP////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ///JxL3/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //+QhHX/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/z4pD/+2raP/////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wBPPCX/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/nZGE////////////d2hV/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + ////////////////////////////////////////dGZU/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP9INRz///////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////U0Ao/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/mo6B////////////hXdn/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/088 + Jf////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AGpb + Rv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/52RhP///////////3doVf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + /////////////4J0ZP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/SDUc//// + //////////////////////////////////////////////////////////////////////////////// + ///////////////////CvLT/U0Ao/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/6idkv///////////3BhTf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/2ZVQf////8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AiX5u/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+dkYT///////// + //93aFX/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////+OhHP/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zwoDf9tXUn/bV1J/21dSf9tXUn/bV1J/21d + Sf9tXUn/bV1J/21dSf9tXUn/bV1J/21dSf9tXUn/bV1J/21dSf9tXUn/bV1J/2JRPf9FMRj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP/Dvrb///////// + //9XRi7/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/4l+bv////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wCvp5z/PCgN/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/nZGE////////////d2hV/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + ////////////////////////////////////////raSY/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/////////////////OyYL/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OyYL/6mglP////8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AM/Lxf9JNh7/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/52RhP///////////3doVf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////85JAn/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/VUMs////////////0MzG/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/STYe/8/Lxf////8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AFVE + Lf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+dkYT///////// + //93aFX/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + ////////////////////////////////////////////////////////////////////////Qi4U/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/5GGdv///////////5uQ + g/84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/VUQt//// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wCpoJT/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/nZGE////////////d2hV/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + /////////////////////////////////////////////39yYP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP////////////////9fTzn/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/6mglP////8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////ADgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/52RhP///////////3doVf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + ///////////////////OyML/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP9zZVL/////////////////OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wBiUT3/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+dkYT///////// + //93aFX/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////3Bh + Tf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP89KQ7/////////////////gXNi/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9jVD//////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8ApJqO/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/nZGE////////////d2hV/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////Qy8X/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/pp2R/////////////////zolCv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/pJuQ/////wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wA4Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/52RhP///////////3doVf84Iwj/OSQJ/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////////8rEvv8+KQ//OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//// + /////////////2ZWQv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8AX043/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+Sh3j///////// + ////////wry0/6adkf+JfGz/bV1J/1A9Jv85JAn/OCMI/zgjCP84Iwj/OCMI/zgjCP85JAn/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/jYFy//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////z8nC/0k2Hv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP87Jgv/opeM/////////////////5aLfP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/19ON/////8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AJ+WiP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/ZVVA//////////////////////////////////// + ///////////////////AubH/pJqO/4Z6af9qW0b/TToi/zgjCP84Iwj/OCMI/zkkCf84Iwj/OCMI/zgj + CP84Iwj/OCMI/42Bcv////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////hnlo/zsm + C/84Iwj/OCMI/zkkCf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP9qW0b//////////////////////6ujlv84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP+floj/////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8AOCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP+PgnT///////////////////////////////////////////////////////// + /////////////////////////////722rf+gl4v/g3hm/2hXQ/9JNh7/OCMI/zgjCP+NgXL///////// + //////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////npSG/2lZRf8/KhD/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/VUMs/4+CdP///////////////////////////5uQ + g/84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AFpIMf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP8+KQ//W0k0/3doVf+Uinv/sKme/87Iwv////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////AubH/sqqg/6adkf+il4z/opeM/6KX + jP+il4z/opeM/6KXjP+il4z/opeM/6KXjP+il4z/opeM/6KXjP+il4z/opeM/6KXjP+imY3/sKme/7+3 + r//OyML//////////////////////////////////////2xcSP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/Wkgx/////wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wCZkIL/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/0EtE/9eTTb/emxa/5eMfv+0rKL/0czH//////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////jIBx/zwoDf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/5mQgv////8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////ADgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/Qy8X/2FPOv98bl3/mZCC/7ewpv////////////////////////////// + //+2r6T///////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////0MzG/6KZjf9lVUD/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wBUQir/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9GMxn/Y1M+/3VoVP9INRz/OCMI/6ujlv////////////// + /////////////////////////////////////////////////////////////62kmP9xYk//PyoQ/z8q + EP9xYk//raSY//////////////////////////////////////////////////////////////////// + ////////sKme/3psWv96bFr/emxa/3psWv96bFr/emxa/3psWv96bFr/emxa/3psWv96bFr/bmBM/2ZV + Qf9OPCT/OyYL/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9UQir/////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AlIh5/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/qJ2S//////////////////////////////////// + //////////////////+il4z/ZlVB/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/WEcw/5SK + e//Py8X/////////////////////////////////////////////////raSY/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/lot8/////wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wA5JAn/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP+elIb////////////////////////////CvLT/iHtq/006Iv84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/0c0G/96bFr/tq2j//// + ////////////////////////pJqO/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI//// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8ATzwl/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/52R + hP//////r6ec/3NlUv89KQ7/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP87Jgv/ZlVB/6KXjP//////opmN/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/048JP////8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AI+C + dP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP+Qg3T/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wDOyML/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/0MzG//// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AEw4IP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/Sjcf/////wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wCKfnD/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/4yAcf////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDCvLT/m5CD/3Nl + Uv9JNR3/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/Qy8X/2ZVQf+NgXL/tq2j/////wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDCvLT/m5CD/3Nl + Uv9JNh7/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP9DLxf/ZlVB/42B + cv+2raP/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDCvLT/m5CD/3Nl + Uv9JNh7/OCMI/zgjCP84Iwj/OCMI/zkkCf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP85JAn/OCMI/zgj + CP84Iwj/OCMI/0MwFv9mVUH/jYFy/7ato/////8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDAubH/mI2A/3Bh + Tf9CLhT/OSQJ/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/Qi4U/3BhTf+YjYD/wLmx/////wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AEw4IP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/STYe/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8ATToi/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/Sjcf/////wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wBRPif/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/Tjwk/////wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AFNA + KP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/Tzwl//// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AVEIq/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/UD0m/////wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wBYRzD/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/VEIq/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AFpIMf84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/VUQt/////wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AW0k0/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/V0Yu/////wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wBcTDb/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/QCsS/3ZnVf+wqZ7/sKme/3ZnVf9AKxL/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/XEw2//// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AGFQO/84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/0k2Hv+Bc2L/vbat/////wD///8A////AP// + /wD///8A////AMnEvf+NgXL/Tzwl/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/XEs1/////wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8AYlE9/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zkkCf84Iwj/OCMI/zkk + Cf9fTzn/m5CD/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8Ap56T/21dSf87Jgv/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgj + CP84Iwj/X043/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wBjVD//OCMI/zgj + CP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP8/KhD/c2VS/6+nnP////8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AL22 + rf+Bc2L/Qy8X/zgjCP84Iwj/OCMI/zgjCP84Iwj/OCMI/zgjCP84Iwj/X085/////wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AGpbRv84Iwj/OCMI/zgjCP84Iwj/UD4m/4h7 + av/CvLT/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDPy8X/lIp7/1dG + Lv84Iwj/OCMI/zgjCP84Iwj/ZlVB/////wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8AbV9K/19POf+bkIP/////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AKeek/9tXUn/bFtH//// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP// + //////////5/////////////////////////+B/////////////////////////gB/////////////// + /////////8AD////////////////////////AAD///////////////////////wAAD////////////// + ////////+AAAD//////////////////////gAAAH/////////////////////4AAAAH///////////// + ////wAf+AAAAAH/gA//////////////AAAAAAAAAAAAD/////////////4AAAAAAAAAAAAH///////// + ////AAAAAAAAAAAAAP////////////8AAAAAAAAAAAAA/////////////gAAAAAAAAAAAAB///////// + ///8AAAAAAAAAAAAAD////////////wAAAAAAAAAAAAAP///////////+AAAAAAAAAAAAAAf//////// + ///4AAAAAAAAAAAAAB////////////AAAAAAAAAAAAAAD///////////4AAAAAAAAAAAAAAH//////// + ///gAAAAAAAAAAAAAAf//////////8AAAAAAAAAAAAAAA///////////gAAAAAAAAAAAAAAB//////// + //+AAAAAAAAAAAAAAAH//////////wAAAAAAAAAAAAAAAP/////////+AAAAAAAAAAAAAAAAf/////// + //gAAAAAAAAAAAAAAAAf////////4AAAAAAAAAAAAAAAAAf///////+AAAAAAAAAAAAAAAAAAf////// + /gAAAAAAAAAAAAAAAAAAf//////4AAAAAAAAAAAAAAAAAAAf/////+AAAAAAAAAAAAAAAAAAAAf///// + AAAAAAAAAAAAAAAAAAAAAP////wAAAAAAAAAAAAAAAAAAAAAP///+AAAAAAAAAAAAAAAAAAAAAAf///4 + AAAAAAAAAAAAAAAAAAAAAB////gAAAAAAAAAAAAAAAAAAAAAH///+AAAAAAAAAAAAAAAAAAAAAAf///4 + AAAAAAAAAAAAAAAAAAAAAB////gAAAAAAAAAAAAAAAAAAAAAH///+AAAAAAAAAAAAAAAAAAAAAAf///4 + AAAAAAAAAAAAAAAAAAAAAB////gAAAAAAAAAAAAAAAAAAAAAH///+AAAAAAAAAAAAAAAAAAAAAAf///4 + AAAAAAAAAAAAAAAAAAAAAB////gAAAAAAAAAAAAAAAAAAAAAH///8AAAAAAAAAAAAAAAAAAAAAAP///w + AAAAAAAAAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAD///8AAAAAAAAAAAAAAAAAAAAAAP///w + AAAAAAAAAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAD///8AAAAAAAAAAAAAAAAAAAAAAP///w + AAAAAAAAAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAD///4AAAAAAAAAAAAAAAAAAAAAAH///A + AAAAAAAAAAAAAAAAAAAAAAP//4AAAAAAAAAAAAAAAAAAAAAAAf//gAAAAAAAAAAAAAAAAAAAAAAB//8A + AAAAAAAAAAAAAAAAAAAAAAD//gAAAAAAAAAAAAAAAAAAAAAAAH/8AAAAAAAAAAAAAAAAAAAAAAAAP/wA + AAAAAAAAAAAAAAAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAB/wAAAAAAAAAAAAAAAAAAAAAAAAD+AA + AAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAA4AA + AAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAYAA + AAAAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAA+AA + AAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAD/AA + AAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAAAAAAAAAH/gA + AAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAAAAAAAP/4A + AAAAAAAAAAAAAAAAAAAAAAB//gAAAAAAAAAAAAAAAAAAAAAAAH//AAAAAAAAAAAAAAAAAAAAAAAA//8A + AAAAAAAAAAAAAAAAAAAAAAD//gAAAAAAAAAAAAAAAAAAAAAAAH/+AAAAAAAAAAAAAAAAAAAAAAAAf/4A + AAAAAAAAAAAAAAAAAAAAAAB//gAAAAAAAAAAAAAAAAAAAAAAAH/+AAAAAAAAAAAAAAAAAAAAAAAAf/4A + AAAAAAAAAAAAAAAAAAAAAAB//AAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAAAAAAAP/wA + AAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAAAAAAAP/wA + AAAAAAAAAAAAAAAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAH/gA + AAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAH/gA + AAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAAAAAAAAAH/wA + AAAAAAAAAAAAAAAAAAAAAAA//gAAAAAAAAAAAAAAAAAAAAAAAH//gAAAAAAAAAAAAAAAAAAAAAAB///A + AAAAAAAAAAAAAAAAAAAAAAP//+AAAAAAAAAAAAAAAAAAAAAAB///8AAAAAAAAAAAAAAAAAAAAAAP///8 + AAAAAAAAAAAAAAAAAAAAAD////4AAAAAAAAAAAAAAAAAAAAAf////wAAAAAAAAAAAAAAAAAAAAD///// + gAAAAAAAAAAAAAAAAAAAAf/////AAAAAAAAAAAAAAAAAAAAD//////AAAAAAAAAAAAAAAAAAAA////// + 8AAAAAAAAAAAAAAAAAAAD//////4AAAAAAAAAAAAAAAAAAAf//////gAAAAAAAAAAAAAAAAAAB////// + +AAAAAAAAAAAAAAAAAAAH//////8AAAAAAAAAAAAAAAAAAA///////wAAAAAAAAAAAAAAAAAAD////// + /AAAAAAAAAAAAAAAAAAAP//////+AAAAAAAAAAAAAAAAAAB///////4AAAAAAAAAAAAAAAAAAH////// + /gAAAAAAAAAAAAAAAAAAf///////AAAAAAAAAAAAAAAAAAD///////8AAAAAAAAAAAAAAAAAAP////// + /wAAAAAAAAAAAAAAAAAA////////gAAAAAAAAAAAAAAAAAH///////+AAAAAAAAAAAAAAAAAAf////// + /4AAAAAAAAAAAAAAAAAB////////gAAAAAAAAAAAAAAAAAH////////AAAAAAAAAAAAAAAAAA/////// + /8AAAAAAAAAAAAAAAAAD////////8AAAAAAAAAAAAAAAAA//////////gAAAAAAAAAAAAAAB//////// + ///8AAAAAAAAAAAAAD/////////////gAAAAAAAAAAAH//////////////4AAAAAAAAAAH////////// + /////wAAAAAAAAAA////////////////gAAAAAAAAAH////////////////AAAAAAAAAA/////////// + /////+AAAAAAAAAH////////////////8AAAAAAAAA/////////////////4AAAAAAAAH/////////// + //////wAAAAAAAA//////////////////gAAAAAAAH//////////////////AAAH4AAA//////////// + //////+AAH/+AAH//////////////////8AD///AA///////////////////4B////gH//////////// + ///////x/////4////////// + + + \ No newline at end of file diff --git a/TGControlPanel/Login.cs b/TGControlPanel/Login.cs index 557d484924..4174c9d85f 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 : CountedForm { + /// + /// Create a form + /// public Login() { InitializeComponent(); @@ -28,48 +31,77 @@ namespace TGControlPanel { IPTextBox.Text = IPTextBox.Text.Trim(); UsernameTextBox.Text = UsernameTextBox.Text.Trim(); - Server.SetRemoteLoginInformation(IPTextBox.Text, (ushort)PortSelector.Value, UsernameTextBox.Text, PasswordTextBox.Text); - var Config = Properties.Settings.Default; - Config.RemoteIP = IPTextBox.Text; - Config.RemoteUsername = UsernameTextBox.Text; - if (SavePasswordCheckBox.Checked) + using (var I = new Interface(IPTextBox.Text, (ushort)PortSelector.Value, UsernameTextBox.Text, PasswordTextBox.Text)) { - Config.RemotePassword = Helpers.EncryptData(PasswordTextBox.Text, out string entrop); - Config.RemoteEntropy = entrop; + var Config = Properties.Settings.Default; + Config.RemoteIP = IPTextBox.Text; + Config.RemotePort = (ushort)PortSelector.Value; + Config.RemoteUsername = UsernameTextBox.Text; + if (SavePasswordCheckBox.Checked) + { + Config.RemotePassword = Helpers.EncryptData(PasswordTextBox.Text, out string entrop); + Config.RemoteEntropy = entrop; + } + else + { + Config.RemotePassword = null; + Config.RemoteEntropy = null; + } + Config.RemoteDefault = true; + VerifyAndConnect(I); } - else - { - Config.RemotePassword = null; - Config.RemoteEntropy = null; - } - Config.RemoteDefault = true; - VerifyAndConnect(); } private void LocalLoginButton_Click(object sender, EventArgs e) { - Server.MakeLocalConnection(); Properties.Settings.Default.RemoteDefault = false; - VerifyAndConnect(); + VerifyAndConnect(new Interface()); } - void VerifyAndConnect() + void VerifyAndConnect(Interface I) { - var res = Server.VerifyConnection(); - if (res != null) + try { - MessageBox.Show("Unable to connect to service! Error: " + res); - return; + var res = I.ConnectionStatus(out string error); + if (!res.HasFlag(ConnectivityLevel.Connected)) + { + MessageBox.Show("Unable to connect to service! Error: " + error); + return; + } + if (!res.HasFlag(ConnectivityLevel.Authenticated)) + { + 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; + } + + if (!res.HasFlag(ConnectivityLevel.Administrator)) + { + while (true) + { + var InstanceToConnectTo = Program.TextPrompt("Select instance", "You do not have permission to list server instances. Please enter the name of the instance to connect to:"); + if (InstanceToConnectTo == null) + return; + + res = I.ConnectToInstance(InstanceToConnectTo); + if (!res.HasFlag(ConnectivityLevel.Connected)) + MessageBox.Show("Unable to connect to instance! Does it exist?"); + else if (!res.HasFlag(ConnectivityLevel.Authenticated)) + MessageBox.Show("The current user is not authorized to access this instance!"); + else + break; + } + + new ControlPanel(I).Show(); + } + else + new InstanceSelector(I).Show(); + Close(); } - if (!Server.Authenticate()) + catch { - 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; + I.Dispose(); + throw; } - Hide(); - using (var M = new Main()) - M.ShowDialog(); - Close(); } private void SavePasswordCheckBox_CheckedChanged(object sender, EventArgs e) diff --git a/TGControlPanel/Main.cs b/TGControlPanel/Main.cs deleted file mode 100644 index 969e926771..0000000000 --- a/TGControlPanel/Main.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Drawing; -using System.Windows.Forms; -using TGServiceInterface; - -namespace TGControlPanel -{ - public partial class Main : Form - { - public Main() - { - InitializeComponent(); - if (Server.VersionMismatch(out string error) && MessageBox.Show(error, "Warning", MessageBoxButtons.OKCancel) == DialogResult.Cancel) - { - Close(); - return; - } - Panels.SelectedIndexChanged += Panels_SelectedIndexChanged; - Panels.SelectedIndex += Math.Min(Properties.Settings.Default.LastPageIndex, Panels.TabCount - 1); - InitRepoPage(); - InitBYONDPage(); - InitServerPage(); - LoadChatPage(); - InitStaticPage(); - } - - private void Main_Resize(object sender, EventArgs e) - { - Panels.Location = new Point(10, 10); - Panels.Width = ClientSize.Width - 20; - Panels.Height = ClientSize.Height - 20; - } - - private void Panels_SelectedIndexChanged(object sender, EventArgs e) - { - switch (Panels.SelectedIndex) - { - case 0: //repo - PopulateRepoFields(); - break; - case 1: //byond - UpdateBYONDButtons(); - break; - case 2: //scp - LoadServerPage(); - break; - case 3: //chat - LoadChatPage(); - break; - } - Properties.Settings.Default.LastPageIndex = Panels.SelectedIndex; - } - } -} diff --git a/TGControlPanel/Program.cs b/TGControlPanel/Program.cs index d5c1ee1988..3ee2151668 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,6 @@ namespace TGControlPanel [STAThread] static void Main(string[] args) { - Server.SetBadCertificateHandler(BadCertificateHandler); try { if (Properties.Settings.Default.UpgradeRequired) @@ -21,8 +19,10 @@ namespace TGControlPanel } Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); - using(var L = new Login()) - Application.Run(L); + Interface.SetBadCertificateHandler(BadCertificateHandler); + var login = new Login(); + login.Show(); + Application.Run(); } catch (Exception e) { @@ -45,16 +45,6 @@ namespace TGControlPanel return true; } - public static bool CheckAdminWithWarning() - { - if (!Server.AuthenticateAdmin()) - { - MessageBox.Show("Only system administrators may use this command!"); - return false; - } - return true; - } - public static void ServiceDisconnectException(Exception e) { MessageBox.Show("An unhandled exception occurred. This usually means we lost connection to the service. Error" + e.ToString()); @@ -68,7 +58,9 @@ namespace TGControlPanel Height = 150, FormBorderStyle = FormBorderStyle.FixedDialog, Text = caption, - StartPosition = FormStartPosition.CenterScreen + StartPosition = FormStartPosition.CenterScreen, + MaximizeBox = false, + MinimizeBox = false, }; Label textLabel = new Label() { Left = 50, Top = 20, Text = text, AutoSize = true }; TextBox textBox = new TextBox() { Left = 50, Top = 50, Width = 400 }; diff --git a/TGControlPanel/TGControlPanel.csproj b/TGControlPanel/TGControlPanel.csproj index 976c556e7d..d1b390395f 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\Debug\ + DEBUG;TRACE + full + AnyCPU + prompt + MinimumRecommendedRules.ruleset + true + + + bin\Release\ + TRACE + bin\x86\Release\TGControlPanel.xml + true + true + pdbonly + AnyCPU + prompt + MinimumRecommendedRules.ruleset + true + @@ -47,23 +49,32 @@ - + Form - + Form + + Form + + + Form + + + InstanceSelector.cs + Form Login.cs - + Form - - Main.cs + + ControlPanel.cs @@ -72,21 +83,24 @@ True Settings.settings - + Form - + Form - + Form + + InstanceSelector.cs + Login.cs - - Main.cs + + ControlPanel.cs diff --git a/TGDreamDaemonBridge/DreamDaemonBridge.cs b/TGDreamDaemonBridge/DreamDaemonBridge.cs new file mode 100644 index 0000000000..c6f401fb89 --- /dev/null +++ b/TGDreamDaemonBridge/DreamDaemonBridge.cs @@ -0,0 +1,37 @@ +using RGiesecke.DllExport; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using TGServiceInterface; +using TGServiceInterface.Components; + +namespace TGDreamDaemonBridge +{ + /// + /// Holds the proc that DD calls to access + /// + public static 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 parsedArgs = new List(); + parsedArgs.AddRange(args); + parsedArgs.RemoveAt(0); + using (var I = new Interface()) + if(I.ConnectToInstance(parsedArgs[0], true).HasFlag(ConnectivityLevel.Connected)) + I.GetComponent().InteropMessage(String.Join(" ", parsedArgs)); + } + catch { } + return 0; + } + } +} diff --git a/TGDreamDaemonBridge/FodyWeavers.xml b/TGDreamDaemonBridge/FodyWeavers.xml new file mode 100644 index 0000000000..43fc6a6308 --- /dev/null +++ b/TGDreamDaemonBridge/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/TGDreamDaemonBridge/Properties/AssemblyInfo.cs b/TGDreamDaemonBridge/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..7195e9f298 --- /dev/null +++ b/TGDreamDaemonBridge/Properties/AssemblyInfo.cs @@ -0,0 +1,16 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("TGStation Server Service DreamDaemon Bridge")] +[assembly: AssemblyDescription("Used by DreamDaemon to call into the TGStation Server Service")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9a01ef03-8eae-45cb-8b87-4a17bd904557")] diff --git a/TGDreamDaemonBridge/TGDreamDaemonBridge.csproj b/TGDreamDaemonBridge/TGDreamDaemonBridge.csproj new file mode 100644 index 0000000000..99acc47c27 --- /dev/null +++ b/TGDreamDaemonBridge/TGDreamDaemonBridge.csproj @@ -0,0 +1,75 @@ + + + + + Debug + AnyCPU + {9A01EF03-8EAE-45CB-8B87-4A17BD904557} + Library + Properties + TGDreamDaemonBridge + TGDreamDaemonBridge + v4.5.2 + 512 + x86 + + + + + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + + + + + + + + + ..\packages\Costura.Fody.1.6.2\lib\dotnet\Costura.dll + False + + + ..\packages\UnmanagedExports.1.2.7\lib\net\RGiesecke.DllExport.Metadata.dll + False + + + + + + + + + + + {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/TGDreamDaemonBridge/packages.config b/TGDreamDaemonBridge/packages.config new file mode 100644 index 0000000000..3d97405485 --- /dev/null +++ b/TGDreamDaemonBridge/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/TGInstallerWrapper/Main.Designer.cs b/TGInstallerWrapper/Main.Designer.cs index 88b82a06a3..885db27148 100644 --- a/TGInstallerWrapper/Main.Designer.cs +++ b/TGInstallerWrapper/Main.Designer.cs @@ -20,6 +20,7 @@ namespace TGInstallerWrapper if (disposing && (components != null)) { components.Dispose(); + CleanTempDir(); } base.Dispose(disposing); } diff --git a/TGInstallerWrapper/Main.cs b/TGInstallerWrapper/Main.cs index e8fbd719d2..ab339ad63e 100644 --- a/TGInstallerWrapper/Main.cs +++ b/TGInstallerWrapper/Main.cs @@ -3,41 +3,32 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using TGServiceInterface; +using TGServiceInterface.Components; namespace TGInstallerWrapper { - public partial class Main : Form + partial class Main : Form { 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 InterfaceClass = InterfaceNamespace + ".Server"; - const string InterfaceServiceInterface = InterfaceNamespace + ".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; + Interface Interface; + + /// + /// Construct an installer form + /// public Main() { InitializeComponent(); SetupTempDir(); - LoadInterfaceFromReflection(); CheckForExistingVersion(); PathTextBox.Text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), DefaultInstallDir); } @@ -50,7 +41,7 @@ namespace TGInstallerWrapper if (File.Exists(tempDir)) File.Delete(tempDir); else if (Directory.Exists(tempDir)) - Directory.Delete(tempDir); + Directory.Delete(tempDir, true); } catch { } if (File.Exists(tempDir) || Directory.Exists(tempDir)) @@ -58,39 +49,37 @@ namespace TGInstallerWrapper 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); + Directory.CreateDirectory(tempDir); } catch { - InterfaceAssembly = null; - VersionLabel.Text = "Error: (Could not load interface dll)"; - return; + tempDir = null; } } + void CleanTempDir() { + if(tempDir != null) + try + { + Directory.Delete(tempDir, true); + } + catch { } + } + void CheckForExistingVersion() { - if (InterfaceAssembly == null) - return; - var verifiedConnection = VerifyConnection.Invoke(null, null) == null; + Interface = new Interface(); + var verifiedConnection = Interface.ConnectionStatus().HasFlag(ConnectivityLevel.Administrator); try { - VersionLabel.Text = (string)Version.Invoke(GetComponentITGSService.Invoke(null, null), null); + VersionLabel.Text = Interface.GetComponent().Version(); + var isV0 = VersionLabel.Text.Contains("v3.0"); + if (isV0) //OH GOD!!!! + MessageBox.Show("Upgrading from version 3.0 may trigger a bug that can delete /config and /data. IT IS STRONGLY RECCOMMENDED THAT YOU BACKUP THESE FOLDERS BEFORE UPDATING!", "Warning"); + if (isV0 || VersionLabel.Text.Contains("v3.1")) + //Friendly reminger + MessageBox.Show("Upgrading to service version 3.2 will break the 3.1 DMAPI. It is recommended you update your game to the 3.2 API before updating the servive to avoid having to trigger hard restarts.", "Note"); } catch { @@ -106,12 +95,10 @@ namespace TGInstallerWrapper bool TellServiceWereComingForThem() { - if (InterfaceAssembly == null) - return ConfirmDangerousUpgrade(); - var connectionVerified = VerifyConnection.Invoke(null, null) == null; + var connectionVerified = Interface.ConnectionStatus().HasFlag(ConnectivityLevel.Administrator); try { - PrepareForUpdate.Invoke(GetComponentITGSService.Invoke(null, null), null); + Interface.GetComponent().PrepareForUpdate(); Thread.Sleep(3000); //chat messages return true; } diff --git a/TGInstallerWrapper/Properties/Resources.Designer.cs b/TGInstallerWrapper/Properties/Resources.Designer.cs index e3858daec9..cd95f1bb4f 100644 --- a/TGInstallerWrapper/Properties/Resources.Designer.cs +++ b/TGInstallerWrapper/Properties/Resources.Designer.cs @@ -79,15 +79,5 @@ 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 daa2b3cb0c..223c76093a 100644 --- a/TGInstallerWrapper/Properties/Resources.resx +++ b/TGInstallerWrapper/Properties/Resources.resx @@ -124,7 +124,4 @@ ..\..\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 4a7e9bf37d..ffee3114b1 100644 --- a/TGInstallerWrapper/TGInstallerWrapper.csproj +++ b/TGInstallerWrapper/TGInstallerWrapper.csproj @@ -14,31 +14,34 @@ - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - app.manifest tgs.ico + + true + bin\Debug\ + DEBUG;TRACE + full + AnyCPU + prompt + MinimumRecommendedRules.ruleset + true + + + bin\Release\ + TRACE + bin\x86\Release\TG Station Server Installer.xml + true + true + pdbonly + AnyCPU + prompt + MinimumRecommendedRules.ruleset + true + ..\packages\Costura.Fody.1.6.2\lib\dotnet\Costura.dll @@ -82,6 +85,12 @@ + + + {ac4e7e8b-f83a-481c-a8b0-8fa4e8ae59ab} + TGServiceInterface + + diff --git a/TGS3Release.ps1 b/TGS3Release.ps1 deleted file mode 100644 index ea5884c247..0000000000 --- a/TGS3Release.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -$bf = $Env:APPVEYOR_BUILD_FOLDER -$src = "$bf\TGInstallerWrapper\bin\Release" -$version = [System.Diagnostics.FileVersionInfo]::GetVersionInfo("$src\TG Station Server Installer.exe").FileVersion - -$destination = "$bf\TGS3-Server-v$version.exe" - -Move-Item -Path "$src\TG Station Server Installer.exe" -Destination "$destination" - -Add-Type -assembly "system.io.compression.filesystem" - -$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/Administration.cs b/TGServerService/Administration.cs deleted file mode 100644 index cc43d818e9..0000000000 --- a/TGServerService/Administration.cs +++ /dev/null @@ -1,282 +0,0 @@ -using System; -using System.DirectoryServices.AccountManagement; -using System.IO; -using System.Security.Principal; -using System.ServiceModel; -using System.Threading; -using TGServiceInterface; - -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 - { - SecurityIdentifier TheDroidsWereLookingFor; - object authLock = new object(); - string LastSeenUser = null; - - readonly SecurityIdentifier ServiceSID = WindowsIdentity.GetCurrent().User; - - /// - public string GetCurrentAuthorizedGroup() - { - try - { - if (TheDroidsWereLookingFor == null) - return "ADMIN"; - - var pc = new PrincipalContext(ContextType.Machine); - return GroupPrincipal.FindByIdentity(pc, IdentityType.Sid, TheDroidsWereLookingFor.Value).Name; - } - catch - { - return null; - } - } - - /// - public string SetAuthorizedGroup(string groupName) - { - if (groupName == null) - { - TheDroidsWereLookingFor = null; - var config = Properties.Settings.Default; - config.AuthorizedGroupSID = null; - config.Save(); - return "ADMIN"; - } - return FindTheDroidsWereLookingFor(groupName); - } - - string FindTheDroidsWereLookingFor(string search = null) - { - //find the group that is authorized to use the tools - var pc = new PrincipalContext(ContextType.Machine); - var config = Properties.Settings.Default; - var groupName = search ?? config.AuthorizedGroupSID; - if (String.IsNullOrWhiteSpace(groupName)) - return null; - var gp = GroupPrincipal.FindByIdentity(pc, search != null ? IdentityType.Name : IdentityType.Sid, groupName); - if (gp == null) - { - if (search != null) - //try again with all types - gp = GroupPrincipal.FindByIdentity(pc, search); - if (gp == null) - return null; - } - TheDroidsWereLookingFor = gp.Sid; - if (search != null) - { - config.AuthorizedGroupSID = TheDroidsWereLookingFor.Value; - config.Save(); - } - 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 - protected override bool CheckAccessCore(OperationContext operationContext) - { - var contract = operationContext.EndpointDispatcher.ContractName; - - if (contract == typeof(ITGConnectivity).Name) //always allow connectivity checks - return true; - - 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; - - var wp = new WindowsPrincipal(windowsIdent); - //first allow admins - var authSuccess = wp.IsInRole(WindowsBuiltInRole.Administrator); - - //if we're not an admin, check that we aren't trying to access the admin interface - if (!authSuccess && operationContext.EndpointDispatcher.ContractName != typeof(ITGAdministration).Name && TheDroidsWereLookingFor != null) - authSuccess = wp.IsInRole(new SecurityIdentifier(Properties.Settings.Default.AuthorizedGroupSID)); - - lock (authLock) - { - var user = windowsIdent.Name; - if (LastSeenUser != user) - { - LastSeenUser = user; - TGServerService.WriteAccess(user, authSuccess); - } - } - return authSuccess; - } - - /// - public ushort RemoteAccessPort() - { - return Properties.Settings.Default.RemoteAccessPort; - } - - /// - public string SetRemoteAccessPort(ushort port) - { - if (port == 0) - return "Cannot bind to port 0"; - var Config = Properties.Settings.Default; - Config.RemoteAccessPort = port; - Config.Save(); - return null; - } - - /// - public string MoveServer(string new_location) - { - var Config = Properties.Settings.Default; - try - { - var di1 = new DirectoryInfo(Config.ServerDirectory); - var di2 = new DirectoryInfo(new_location); - - var copy = di1.Root.FullName != di2.Root.FullName; - - if (copy && File.Exists(PrivateKeyPath)) - return String.Format("Unable to perform a cross drive server move with the {0}. Copy aborted!", PrivateKeyPath); - - new_location = di2.FullName; - - while (di2.Parent != null) - if (di2.Parent.FullName == di1.FullName) - return "Cannot move to child of current directory!"; - else - di2 = di2.Parent; - - if (!Monitor.TryEnter(RepoLock)) - return "Repo locked!"; - try - { - if (RepoBusy) - return "Repo busy!"; - DisposeRepo(); - if (!Monitor.TryEnter(ByondLock)) - return "BYOND locked"; - try - { - if (updateStat != TGByondStatus.Idle) - return "BYOND busy!"; - if (!Monitor.TryEnter(CompilerLock)) - return "Compiler locked!"; - - try - { - if (compilerCurrentStatus != TGCompilerStatus.Uninitialized && compilerCurrentStatus != TGCompilerStatus.Initialized) - return "Compiler busy!"; - if (!Monitor.TryEnter(watchdogLock)) - return "Watchdog locked!"; - try - { - if (currentStatus != TGDreamDaemonStatus.Offline) - return "Watchdog running!"; - lock (configLock) - { - CleanGameFolder(); - Program.DeleteDirectory(GameDir); - string error = null; - if (copy) - { - Program.CopyDirectory(Config.ServerDirectory, new_location); - Directory.CreateDirectory(new_location); - Environment.CurrentDirectory = new_location; - try - { - Program.DeleteDirectory(Config.ServerDirectory); - } - catch (Exception e) - { - error = "The move was successful, but the path " + Config.ServerDirectory + " was unable to be deleted fully!"; - TGServerService.WriteWarning(String.Format("Server move from {0} to {1} partial success: {2}", Config.ServerDirectory, new_location, e.ToString()), TGServerService.EventID.ServerMovePartial); - } - } - else - { - try - { - Environment.CurrentDirectory = di2.Root.FullName; - Directory.Move(Config.ServerDirectory, new_location); - Environment.CurrentDirectory = new_location; - } - catch - { - Environment.CurrentDirectory = Config.ServerDirectory; - throw; - } - } - TGServerService.WriteInfo(String.Format("Server moved from {0} to {1}", Config.ServerDirectory, new_location), TGServerService.EventID.ServerMoveComplete); - Config.ServerDirectory = new_location; - return null; - } - } - finally - { - Monitor.Exit(watchdogLock); - } - } - finally - { - Monitor.Exit(CompilerLock); - } - } - finally - { - Monitor.Exit(ByondLock); - } - } - finally - { - Monitor.Exit(RepoLock); - } - } - catch (Exception e) - { - TGServerService.WriteError(String.Format("Server move from {0} to {1} failed: {2}", Config.ServerDirectory, new_location, e.ToString()), TGServerService.EventID.ServerMoveFailed); - return e.ToString(); - } - } - public string RecreateStaticFolder() - { - if (!Monitor.TryEnter(RepoLock)) - return "Repo locked!"; - try - { - if (!Monitor.TryEnter(watchdogLock)) - return "Watchdog locked!"; - try - { - if (!Monitor.TryEnter(configLock)) - return "Static dir locked!"; - try - { - if (currentStatus != TGDreamDaemonStatus.Offline) - return "Watchdog running!"; - BackupAndDeleteStaticDirectory(); - InitialConfigureRepository(); - } - finally - { - Monitor.Exit(configLock); - } - } - finally - { - Monitor.Exit(watchdogLock); - } - } - catch(Exception e) - { - return e.ToString(); - } - finally - { - Monitor.Exit(RepoLock); - } - return null; - } - } -} diff --git a/TGServerService/AdministrativeAuthorizationManager.cs b/TGServerService/AdministrativeAuthorizationManager.cs new file mode 100644 index 0000000000..e5aaf062d5 --- /dev/null +++ b/TGServerService/AdministrativeAuthorizationManager.cs @@ -0,0 +1,37 @@ +using System; +using System.Diagnostics; +using System.Security.Principal; +using System.ServiceModel; +using TGServiceInterface.Components; + +namespace TGServerService +{ + /// + /// A used to determine only if the caller is an admin + /// + sealed class AdministrativeAuthorizationManager : ServiceAuthorizationManager + { + string LastSeenUser; + protected override bool CheckAccessCore(OperationContext operationContext) + { + var contract = operationContext.EndpointDispatcher.ContractName; + + if (contract == typeof(ITGConnectivity).Name) //always allow connectivity checks + return true; + + var windowsIdent = operationContext.ServiceSecurityContext.WindowsIdentity; + + var wp = new WindowsPrincipal(windowsIdent); + //first allow admins + var authSuccess = wp.IsInRole(WindowsBuiltInRole.Administrator); + + var user = windowsIdent.Name; + if (LastSeenUser != user) + { + LastSeenUser = user; + Service.WriteEntry(String.Format("Root access from: {0}", user), EventID.Authentication, authSuccess ? EventLogEntryType.SuccessAudit : EventLogEntryType.FailureAudit, Service.LoggingID); + } + return authSuccess; + } + } +} diff --git a/TGServerService/App.config b/TGServerService/App.config index f17feaf7ae..42fede526f 100644 --- a/TGServerService/App.config +++ b/TGServerService/App.config @@ -2,6 +2,7 @@ +
@@ -10,75 +11,18 @@ - - tgstation - - - 1337 - - - False - - - tgstation-server - - - tgstation-server@tgstation13.org - - - C:/tgstation-server-3 - - - 0 - - - False - C:\Python27 - - NEEDS INITIALIZING - - - - True - - True - - - 0 - - - 0 - - 5 - - - - - - False - - - + 7 38607 - - - - - 0 - - - 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 deleted file mode 100644 index 0f6db947b7..0000000000 --- a/TGServerService/ChatCommands.cs +++ /dev/null @@ -1,193 +0,0 @@ -using TGServiceInterface; -using System; -using System.Collections.Generic; -using System.Threading; - -namespace TGServerService -{ - class CommandInfo - { - public bool IsAdmin { get; set; } - public bool IsAdminChannel { get; set; } - public string Speaker { get; set; } - public TGStationServer Server { get; set; } - } - abstract class ChatCommand : Command - { - public bool RequiresAdmin { get; protected set; } - public static ThreadLocal CommandInfo = new ThreadLocal(); - protected TGStationServer Instance { get { return CommandInfo.Value.Server; } } - public override ExitCode DoRun(IList parameters) - { - if (RequiresAdmin) - { - var Info = CommandInfo.Value; - if (!Info.IsAdmin) - { - OutputProc("You are not authorized to use that command!"); - return ExitCode.BadCommand; - } - if (!Info.IsAdminChannel) - { - OutputProc("Use this command in an admin channel!"); - return ExitCode.BadCommand; - } - } - return base.DoRun(parameters); - } - } - - class ServerChatCommand : ChatCommand - { - readonly string HelpText; - public ServerChatCommand(string name, string helpText, bool adminOnly, int requiredParameters) - { - Keyword = name; - RequiresAdmin = adminOnly; - HelpText = helpText; - 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)))); - if (res != "SUCCESS" && !String.IsNullOrWhiteSpace(res)) - OutputProc(res); - return ExitCode.Normal; - } - } - - class RootChatCommand : RootCommand - { - public RootChatCommand(List serverCommands) - { - var tmp = new List { new PRsCommand(), new VersionCommand(), new RevisionCommand(), new ByondCommand(), new KekCommand() }; - if (serverCommands != null) - tmp.AddRange(serverCommands); - Children = tmp.ToArray(); - serverCommands = new List(); - PrintHelpList = true; - } - } - class RevisionCommand : ChatCommand - { - public RevisionCommand() - { - Keyword = "revision"; - } - protected override ExitCode Run(IList parameters) - { - var res = Instance.LiveSha(); - if (res == "UNKNOWN") { - OutputProc(res); - return ExitCode.ServerError; - } - OutputProc(String.Format("^{0}", res)); - return ExitCode.Normal; - } - - public override string GetHelpText() - { - return "Prints the current code revision of the repository (not the server)"; - } - } - - class ByondCommand : ChatCommand - { - public ByondCommand() - { - Keyword = "byond"; - } - protected override ExitCode Run(IList parameters) - { - var type = TGByondVersion.Installed; - if (parameters.Count > 0) - if (parameters[0].ToLower() == "--staged") - type = TGByondVersion.Staged; - else if (parameters[0].ToLower() == "--latest") - type = TGByondVersion.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 - { - 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 - { - public KekCommand() - { - Keyword = "kek"; - } - protected override ExitCode Run(IList parameters) - { - OutputProc("kek"); - return ExitCode.Normal; - } - - public override string GetHelpText() - { - return "kek"; - } - } - class PRsCommand : ChatCommand - { - public PRsCommand() - { - Keyword = "prs"; - } - protected override ExitCode Run(IList parameters) - { - var PRs = Instance.MergedPullRequests(out string res); - if (PRs == null) - { - OutputProc(res); - return ExitCode.ServerError; - } - if (PRs.Count == 0) - OutputProc("None!"); - else - { - res = ""; - foreach (var I in PRs) - res += "#" + I.Number + " "; - OutputProc(res); - } - return ExitCode.Normal; - } - - public override string GetHelpText() - { - return "Gets the currently merged pull requests in the repository"; - } - } - -} diff --git a/TGServerService/ChatCommands/ByondCommand.cs b/TGServerService/ChatCommands/ByondCommand.cs new file mode 100644 index 0000000000..27cd19dadf --- /dev/null +++ b/TGServerService/ChatCommands/ByondCommand.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using TGServiceInterface; + +namespace TGServerService.ChatCommands +{ + /// + /// 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 = ByondVersion.Installed; + if (parameters.Count > 0) + if (parameters[0].ToLower() == "--staged") + type = ByondVersion.Staged; + else if (parameters[0].ToLower() == "--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]"; + } + } +} diff --git a/TGServerService/ChatCommands/ChatCommand.cs b/TGServerService/ChatCommands/ChatCommand.cs new file mode 100644 index 0000000000..17794613fa --- /dev/null +++ b/TGServerService/ChatCommands/ChatCommand.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Threading; +using TGServiceInterface; + +namespace TGServerService.ChatCommands +{ + /// + /// 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; } + /// + /// Shorthand for accessing + /// + protected ServerInstance Instance { get { return CommandInfo.Value.Server; } } + + /// + public override ExitCode DoRun(IList parameters) + { + if (RequiresAdmin) + { + var Info = CommandInfo.Value; + if (!Info.IsAdmin) + { + OutputProc("You are not authorized to use that command!"); + return ExitCode.BadCommand; + } + if (!Info.IsAdminChannel) + { + OutputProc("Use this command in an admin channel!"); + return ExitCode.BadCommand; + } + } + return base.DoRun(parameters); + } + } +} diff --git a/TGServerService/ChatCommands/CommandInfo.cs b/TGServerService/ChatCommands/CommandInfo.cs new file mode 100644 index 0000000000..913bd50e70 --- /dev/null +++ b/TGServerService/ChatCommands/CommandInfo.cs @@ -0,0 +1,25 @@ +namespace TGServerService.ChatCommands +{ + /// + /// 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; } + /// + /// A reference to the that runs the that heard the + /// + public ServerInstance Server { get; set; } + } +} diff --git a/TGServerService/ChatCommands/KekCommand.cs b/TGServerService/ChatCommands/KekCommand.cs new file mode 100644 index 0000000000..1c2eda51c6 --- /dev/null +++ b/TGServerService/ChatCommands/KekCommand.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace TGServerService.ChatCommands +{ + /// + /// 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"; + } + } +} diff --git a/TGServerService/ChatCommands/PullRequestsCommand.cs b/TGServerService/ChatCommands/PullRequestsCommand.cs new file mode 100644 index 0000000000..50495174c5 --- /dev/null +++ b/TGServerService/ChatCommands/PullRequestsCommand.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; + +namespace TGServerService.ChatCommands +{ + /// + /// Retrieve the list of test-merged github pull requests + /// + sealed class PullRequestsCommand : ChatCommand + { + /// + /// Construct a + /// + public PullRequestsCommand() + { + Keyword = "prs"; + } + + /// + protected override ExitCode Run(IList parameters) + { + var PRs = Instance.MergedPullRequests(out string res); + if (PRs == null) + { + OutputProc(res); + return ExitCode.ServerError; + } + if (PRs.Count == 0) + OutputProc("None!"); + else + { + res = ""; + foreach (var I in PRs) + res += "#" + I.Number + " "; + OutputProc(res); + } + return ExitCode.Normal; + } + + /// + public override string GetHelpText() + { + return "Gets the currently merged pull requests in the repository"; + } + } +} diff --git a/TGServerService/ChatCommands/RevisionCommand.cs b/TGServerService/ChatCommands/RevisionCommand.cs new file mode 100644 index 0000000000..18b28b96ac --- /dev/null +++ b/TGServerService/ChatCommands/RevisionCommand.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace TGServerService.ChatCommands +{ + /// + /// 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(); + if (res == "UNKNOWN") + { + OutputProc(res); + return ExitCode.ServerError; + } + OutputProc(String.Format("^{0}", res)); + return ExitCode.Normal; + } + + /// + public override string GetHelpText() + { + return "Prints the current code revision of the repository (not the server)"; + } + } +} diff --git a/TGServerService/ChatCommands/RootChatCommand.cs b/TGServerService/ChatCommands/RootChatCommand.cs new file mode 100644 index 0000000000..82bb3e172e --- /dev/null +++ b/TGServerService/ChatCommands/RootChatCommand.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using TGServiceInterface; + +namespace TGServerService.ChatCommands +{ + /// + /// 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 PullRequestsCommand(), new VersionCommand(), new RevisionCommand(), new ByondCommand(), new KekCommand() }; + if (serverCommands != null) + tmp.AddRange(serverCommands); + Children = tmp.ToArray(); + serverCommands = new List(); + PrintHelpList = true; + } + } +} diff --git a/TGServerService/ChatCommands/ServerChatCommand.cs b/TGServerService/ChatCommands/ServerChatCommand.cs new file mode 100644 index 0000000000..f7551e60af --- /dev/null +++ b/TGServerService/ChatCommands/ServerChatCommand.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; + +namespace TGServerService.ChatCommands +{ + /// + /// 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; + RequiresAdmin = adminOnly; + HelpText = helpText; + 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, Program.SanitizeTopicString(String.Join(" ", parameters)))); + if (res != "SUCCESS" && !String.IsNullOrWhiteSpace(res)) + OutputProc(res); + return ExitCode.Normal; + } + } +} diff --git a/TGServerService/ChatCommands/VersionCommand.cs b/TGServerService/ChatCommands/VersionCommand.cs new file mode 100644 index 0000000000..d066d83971 --- /dev/null +++ b/TGServerService/ChatCommands/VersionCommand.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace TGServerService.ChatCommands +{ + /// + /// 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"; + } + } +} 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 62% rename from TGServerService/Discord.cs rename to TGServerService/ChatProviders/DiscordChatProvider.cs index 87758d74e5..1b347b0de2 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,6 @@ 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); return null; } } @@ -185,6 +225,10 @@ namespace TGServerService return e.ToString(); } } + + /// + /// Shutsdown and disposes + /// void DisconnectAndDispose() { try @@ -192,20 +236,19 @@ namespace TGServerService client.StopAsync().Wait(); client.LogoutAsync().Wait(); } - catch (Exception e) { - TGServerService.WriteError("Discord failed DnD: " + e.ToString(), TGServerService.EventID.ChatDisconnectFail); - } + catch { } 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 +271,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 +302,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 64% rename from TGServerService/IRC.cs rename to TGServerService/ChatProviders/IRCChatProvider.cs index a04dfc2d36..a41a7fd72b 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,6 @@ 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); return null; } catch (Exception e) @@ -64,9 +82,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,6 +103,11 @@ 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) @@ -122,12 +146,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(); @@ -144,7 +177,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 @@ -165,7 +201,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) @@ -174,7 +213,7 @@ namespace TGServerService irc.SendMessage(SendType.Message, IRCConfig.AuthTarget, IRCConfig.AuthMessage); } } - //public api + /// public string Connect() { if (Connected() || !IRCConfig.Enabled) @@ -213,7 +252,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()) @@ -224,16 +265,16 @@ namespace TGServerService catch { } } - //public api + /// public string Reconnect() { Disconnect(); return Connect(); } - //public api + /// public void Disconnect() - { + { try { lock (IRCLock) @@ -245,12 +286,9 @@ namespace TGServerService } } } - catch (Exception e) - { - TGServerService.WriteError("IRC failed QnD: " + e.ToString(), TGServerService.EventID.ChatDisconnectFail); - } + catch { } } - //public api + /// public bool Connected() { lock (IRCLock) @@ -258,30 +296,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) { @@ -306,6 +354,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 new file mode 100644 index 0000000000..152962fdfa --- /dev/null +++ b/TGServerService/DeprecatedInstanceConfig.cs @@ -0,0 +1,119 @@ +using System; +using System.Configuration; + +namespace TGServerService +{ + /// + /// Used to migrate old config settings + /// + class DeprecatedInstanceConfig : InstanceConfig + { + /// + /// The default directory + /// + const string DefaultInstallationPath = "C:\\tgstation-server-3"; + + /// + /// Convert the settings version 6 .NET settings file to a config json + /// + /// An based off the old .NET setting file + public static InstanceConfig CreateFromNETSettings() + { + var Config = Properties.Settings.Default; + var result = new DeprecatedInstanceConfig(LoadPreviousNetPropertyOrDefault("ServerDirectory", "C:\\tgstation-server-3")); + // using nameof for sanity where possible + result.ProjectName = LoadPreviousNetPropertyOrDefault(nameof(ProjectName), result.ProjectName); + result.Port = LoadPreviousNetPropertyOrDefault("ServerPort", result.Port); + result.CommitterName = LoadPreviousNetPropertyOrDefault(nameof(CommitterName), result.CommitterName); + result.CommitterEmail = LoadPreviousNetPropertyOrDefault(nameof(CommitterEmail), result.CommitterEmail); + result.Security = LoadPreviousNetPropertyOrDefault("ServerSecurity", result.Security); + result.Autostart = LoadPreviousNetPropertyOrDefault("DDAutoStart", result.Autostart); + result.ChatProviderData = LoadPreviousNetPropertyOrDefault(nameof(ChatProviderData), result.ChatProviderData); + result.ChatProviderEntropy = LoadPreviousNetPropertyOrDefault(nameof(ChatProviderEntropy), result.ChatProviderEntropy); + result.ReattachRequired = LoadPreviousNetPropertyOrDefault("ReattachToDD", result.ReattachRequired); + result.ReattachProcessID = LoadPreviousNetPropertyOrDefault("ReattachPID", result.ReattachProcessID); + result.ReattachPort = LoadPreviousNetPropertyOrDefault(nameof(ReattachPort), result.ReattachPort); + result.ReattachCommsKey = LoadPreviousNetPropertyOrDefault(nameof(ReattachCommsKey), result.ReattachCommsKey); + result.ReattachAPIVersion = LoadPreviousNetPropertyOrDefault(nameof(ReattachAPIVersion), result.ReattachAPIVersion); + result.AutoUpdateInterval = LoadPreviousNetPropertyOrDefault(nameof(AutoUpdateInterval), result.AutoUpdateInterval); + result.AuthorizedUserGroupSID = LoadPreviousNetPropertyOrDefault("AuthorizedGroupSID", result.AuthorizedUserGroupSID); + result.MigrateToCurrentVersion(); + return result; + } + + /// + /// Loads a previous .NET config and returns it or some if it wasn't set + /// + /// The type of the + /// The config key of the property + /// The default value of the + /// The if it exists, otherwise the + static T LoadPreviousNetPropertyOrDefault(string property, T defaultValue) + { + //try it the simple way first + try + { + var result = (T)Properties.Settings.Default.GetPreviousVersion(property); + return result == null ? defaultValue : result; + } + catch + { + try + { + //.NET is fucking stupid + //If we don't have the correct property in our *CURRENT* .settings file it will automatically throw an exception when it tries to load it + //Which means we can never fucking delete config settings + //Which is fucking retarded + //This hooks into the settings provider and forces it to load it anyway + var Config = Properties.Settings.Default; + var Provider = Config.Properties[nameof(Config.SettingsVersion)].Provider; //nameof for sanity + + var sp = new SettingsProperty(property) + { + PropertyType = typeof(T), + DefaultValue = defaultValue, + Provider = Provider + }; + + var ProviderInterface = Provider as IApplicationSettingsProvider; + + var result = ProviderInterface.GetPreviousVersion(Config.Context, sp); + if (result != null && result.PropertyValue != null) + return (T)result.PropertyValue; + } + catch { } + //f u c k i t + return defaultValue; + } + } + + /// + /// Construct a . Used by the deserializer + /// + [Obsolete("This method is for use by the deserializer only.", true)] + public DeprecatedInstanceConfig() : base(DefaultInstallationPath) { } + + /// + /// Construct a for a at + /// + /// The path to the + public DeprecatedInstanceConfig(string path) : base(path) { } + + /// + /// 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 d7d5226721..0000000000 --- a/TGServerService/DreamDaemon.cs +++ /dev/null @@ -1,615 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Reflection; -using System.Threading; -using System.Timers; -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 string DiagnosticsDir = "Diagnostics"; - const string ResourceDiagnosticsDir = DiagnosticsDir + "/Resources"; - const int DDHangStartTime = 60; - const int DDBadStartTime = 10; - - Process Proc; - PerformanceCounter pcpu; - - object watchdogLock = new object(); - Thread DDWatchdog; - TGDreamDaemonStatus currentStatus; - string CurrentDDLog; - ushort currentPort = 0; - - object restartLock = new object(); - bool RestartInProgress = false; - - TGDreamDaemonSecurity StartingSecurity; - - ShutdownRequestPhase AwaitingShutdown; - - //Only need 1 proc instance - void InitDreamDaemon() - { - Directory.CreateDirectory(DiagnosticsDir); - Directory.CreateDirectory(ResourceDiagnosticsDir); - var Reattach = Properties.Settings.Default.ReattachToDD; - if (Reattach) - try - { - Proc = Process.GetProcessById(Properties.Settings.Default.ReattachPID); - if (Proc == null) - throw new Exception("GetProcessById returned null!"); - TGServerService.WriteInfo("Reattached to running DD process!", TGServerService.EventID.DDReattachSuccess); - ThreadPool.QueueUserWorkItem(_ => - { - Thread.Sleep(5000); - SendMessage("DD: Update complete. Watch dog reactivated...", ChatMessageType.WatchdogInfo); - }); - - //start wd - RestartInProgress = true; - currentPort = Properties.Settings.Default.ReattachPort; - serviceCommsKey = Properties.Settings.Default.ReattachCommsKey; - try - { - GameAPIVersion = new Version(Properties.Settings.Default.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}", Properties.Settings.Default.ReattachPID, e.ToString()), TGServerService.EventID.DDReattachFail); - } - finally - { - Properties.Settings.Default.ReattachToDD = false; - Properties.Settings.Default.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 (Properties.Settings.Default.DDAutoStart) - //break this off so we don't hold up starting the service - ThreadPool.QueueUserWorkItem( _ => { Start(); }); - } - - //die now k thx - void DisposeDreamDaemon() - { - var Detach = Properties.Settings.Default.ReattachToDD; - bool RenameLog = false; - if (DaemonStatus() == TGDreamDaemonStatus.Online) - { - if (!Detach) - { - WorldAnnounce("Server service stopped"); - Thread.Sleep(1000); - } - else - { - RenameLog = CurrentDDLog != null; - SendMessage("DD: Detaching watch dog for update!", ChatMessageType.WatchdogInfo); - WriteCurrentDDLog("Service updating! Splitting diagnostics..."); - } - } - else if (Detach) - Properties.Settings.Default.ReattachToDD = false; - Stop(); - if(pcpu != null) - pcpu.Dispose(); - if (RenameLog) - try - { - File.Move(Path.Combine(ResourceDiagnosticsDir, CurrentDDLog), Path.Combine(ResourceDiagnosticsDir, "SU-" + CurrentDDLog)); - } - catch { } - } - - //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) - { - Properties.Settings.Default.ServerPort = 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; - } - - void WriteCurrentDDLog(string message) - { - lock (watchdogLock) - { - if (currentStatus != TGDreamDaemonStatus.Online || CurrentDDLog == null) - return; - File.AppendAllText(Path.Combine(ResourceDiagnosticsDir, CurrentDDLog), String.Format("[{0}]: {1}\n", DateTime.Now.ToLongTimeString(), message)); - } - } - - //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; - - 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(); - Proc.WaitForExit(); - lock (watchdogLock) //synchronize - { - MemTrackTimer.Stop(); - pcpu.Dispose(); - } - - bool BadStart; - lock (watchdogLock) - { - currentStatus = TGDreamDaemonStatus.HardRebooting; - currentPort = 0; - Proc.Close(); - - if (AwaitingShutdown == ShutdownRequestPhase.Pinged) - return; - 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), 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); - } - } - if (BadStart) - WriteCurrentDDLog("Crash detected!"); - - 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 (!Properties.Settings.Default.ReattachToDD) - { - Proc.Kill(); - Proc.WaitForExit(); - } - else - { - Properties.Settings.Default.ReattachPID = Proc.Id; - Properties.Settings.Default.ReattachPort = currentPort; - Properties.Settings.Default.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(!Properties.Settings.Default.ReattachToDD) - 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); - } - } - } - - 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 api - public string CanStart() - { - lock (watchdogLock) - { - return CanStartImpl(); - } - } - - string CanStartImpl() - { - if (GetVersion(TGByondVersion.Installed) == null) - return "Byond is not installed!"; - var DMB = GameDirLive + "/" + Properties.Settings.Default.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)Properties.Settings.Default.ServerSecurity; - 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 Config = Properties.Settings.Default; - var DMB = GameDirLive + "/" + Config.ProjectName + ".dmb"; - - GenCommsKey(); - StartingSecurity = (TGDreamDaemonSecurity)Config.ServerSecurity; - Proc.StartInfo.Arguments = String.Format("{0} -port {1} {5}-close -verbose -params \"server_service={3}&server_service_version={4}\" -{2} -public", DMB, Config.ServerPort, SecurityWord(), serviceCommsKey, Version(), Config.Webclient ? "-webclient " : ""); - UpdateInterfaceDll(true); - lock (topicLock) - { - 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.ServerPort; - 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() - { - lock (watchdogLock) - { - return (TGDreamDaemonSecurity)Properties.Settings.Default.ServerSecurity; - } - } - - //public api - public bool SetSecurityLevel(TGDreamDaemonSecurity level) - { - var Config = Properties.Settings.Default; - var secInt = (int)level; - bool needReboot; - lock (watchdogLock) - { - needReboot = Config.ServerSecurity != secInt; - Config.ServerSecurity = secInt; - } - if (needReboot) - RequestRestart(); - return DaemonStatus() != TGDreamDaemonStatus.Online; - } - - //public api - public bool Autostart() - { - return Properties.Settings.Default.DDAutoStart; - } - - //public api - public void SetAutostart(bool on) - { - Properties.Settings.Default.DDAutoStart = 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 Properties.Settings.Default.ServerPort; - } - - //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 Properties.Settings.Default.Webclient; - } - - /// - public void SetWebclient(bool on) - { - var Config = Properties.Settings.Default; - 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..1c4f893bdc --- /dev/null +++ b/TGServerService/EventID.cs @@ -0,0 +1,335 @@ +using System; + +namespace TGServerService +{ + /// + /// Various events and their IDs in no particular order. Found in the Windows event log. These key incremented by 100 and are guaranteed to never be reused in the future. In the windows event viewer, these IDs will be offset by the to distinguish events between instances. 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 TGServiceInterface.Components.ITGAdministration.MoveServer(string) + /// + [Obsolete("Not in use anymore", true)] + ServerMoveFailed = 900, + /// + /// Warning: Failed to delete the old directory during a TGServiceInterface.Components.ITGAdministration.MoveServer(string) operation + /// + [Obsolete("Not in use anymore", true)] + ServerMovePartial = 1000, + /// + /// Info: Successful completion of a TGServiceInterface.Components.ITGAdministration.MoveServer(string) operation + /// + [Obsolete("Not in use anymore", true)] + 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 + /// + [Obsolete("Not in use anymore", true)] + ChatSend = 2600, + /// + /// Info: Successful completion of a operation + /// + [Obsolete("Not in use anymore", true)] + ChatBroadcast = 2700, + /// + /// Not in use anymore + /// + [Obsolete("Not in use anymore", true)] + ChatAdminBroadcast = 2800, + /// + /// Error: When an error occurs during a operation + /// + [Obsolete("Not in use anymore", true)] + 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 + /// + BridgeDLLUpdated = 5800, + /// + /// Error: An error occurred while updating the dll for the + /// + BridgeDLLUpdateFail = 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 + /// Error: When a submodule update operation fails completely + /// + Submodule = 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, + /// + /// Info: When an instance's logging ID is first assigned + /// + InstanceIDAssigned = 7600, + /// + /// Info: When a testmerge commit is published + /// Warning: When a testmerge commit failed to be published + /// + ReferencePush = 7700, + } +} diff --git a/TGServerService/InstanceConfig.cs b/TGServerService/InstanceConfig.cs new file mode 100644 index 0000000000..3911ec69e7 --- /dev/null +++ b/TGServerService/InstanceConfig.cs @@ -0,0 +1,158 @@ +using System.IO; +using System.Web.Script.Serialization; +using TGServiceInterface; + +namespace TGServerService +{ + class InstanceConfig + { + /// + /// The name the file is saved as in the + /// + //tell javascriptserializer to ignore these fields + [ScriptIgnore] + public const string JSONFilename = "Instance.json"; + /// + /// The current version of the config + /// + [ScriptIgnore] + protected const ulong CurrentVersion = 0; //Literally any time you add/deprecated a field, this number needs to be bumped + /// + /// The directory this is for + /// + [ScriptIgnore] + public string Directory { get; private set; } + + /// + /// Actual version of the . Migrated up via + /// + public ulong Version { get; protected set; } = CurrentVersion; + + /// + /// The name of the + /// + public string Name { get; set; } = "TG Station Server"; + + /// + /// If the is active + /// + public bool Enabled { get; set; } = true; + + /// + /// The name of the .dme/.dmb the uses + /// + public string ProjectName { get; set; } = "tgstation"; + + /// + /// The port the runs on + /// + public ushort Port { get; set; } = 1337; + + /// + /// The level for the + /// + public DreamDaemonSecurity Security { get; set; } = DreamDaemonSecurity.Trusted; + + /// + /// Whether or not the should immediately start DreamDaemon when activated + /// + public bool Autostart { get; set; } = false; + + /// + /// Whether or not DreamDaemon allows connections from webclients + /// + public bool Webclient { get; set; } = false; + + /// + /// Author and committer name for synchronize commits + /// + public string CommitterName { get; set; } = "tgstation-server"; + /// + /// Author and committer e-mail for synchronize commits + /// + public string CommitterEmail { get; set; } = "tgstation-server@tgstation13.org"; + + /// + /// Encrypted serialized s + /// + public string ChatProviderData { get; set; } = ServerInstance.UninitializedString; + + /// + /// Entropy for + /// + public string ChatProviderEntropy { get; set; } + + /// + /// If the should reattach to a running DreamDaemon + /// + public bool ReattachRequired { get; set; } = false; + + /// + /// The of the runnning DreamDaemon + /// + public int ReattachProcessID { get; set; } + + /// + /// The port the runnning DreamDaemon was launched on + /// + public ushort ReattachPort { get; set; } + + /// + /// The serviceCommsKey the runnning DreamDaemon was launched on + /// + public string ReattachCommsKey { get; set; } + + /// + /// The API version of the runnning DreamDaemon + /// + public string ReattachAPIVersion { get; set; } + + /// + /// The user group allowed to use the + /// + public string AuthorizedUserGroupSID { get; set; } = null; + + /// + /// The auto update interval for the + /// + public ulong AutoUpdateInterval { get; set; } = 0; + + /// + /// Whether or not testmerge commits are published to a temporary remote branch + /// + public bool PushTestmergeCommits { get; set; } = false; + + /// + /// Construct a for a at + /// + /// The path to the + public InstanceConfig(string path) + { + Directory = path; + } + + /// + /// Saves the to it's + /// + public void Save() + { + var data = new JavaScriptSerializer().Serialize(this); + var path = Path.Combine(Directory, JSONFilename); + File.WriteAllText(path, data); + } + + /// + /// Loads and migrates an from a at + /// + /// The path to the directory + /// The migrated + public static InstanceConfig Load(string path) + { + var configtext = File.ReadAllText(Path.Combine(path, JSONFilename)); + var res = new JavaScriptSerializer().Deserialize(configtext); + res.Directory = path; + res.MigrateToCurrentVersion(); + return res; + } + } +} diff --git a/TGServerService/InterfaceBase.cs b/TGServerService/InterfaceBase.cs deleted file mode 100644 index 6728d5c78f..0000000000 --- a/TGServerService/InterfaceBase.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.ServiceModel; -using TGServiceInterface; - -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 - - //this line basically says make one instance of the service, use it multithreaded for requests, and never delete it - [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.Single)] - partial class TGStationServer : IDisposable, ITGSService, ITGConnectivity - { - - //call partial constructors/destructors from here - //called when the service is started - public TGStationServer() - { - FindTheDroidsWereLookingFor(); - InitChat(); - InitRepo(); - InitByond(); - InitCompiler(); - InitDreamDaemon(); - } - - //called when the service is stopped - void RunDisposals() - { - DisposeDreamDaemon(); - DisposeCompiler(); - DisposeByond(); - DisposeRepo(); - DisposeChat(); - } - - //public api - public string Version() - { - return TGServerService.Version; - } - - //public api - public void VerifyConnection() { } - - //public api - public void PrepareForUpdate() - { - Properties.Settings.Default.ReattachToDD = true; - SendMessage("SERVICE: Update started...", ChatMessageType.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 - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - RunDisposals(); - // TODO: dispose managed state (managed objects). - } - - // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. - // TODO: set large fields to null. - - disposedValue = true; - } - } - - // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. - // ~TGStationServer() { - // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - // Dispose(false); - // } - - // This code added to correctly implement the disposable pattern. - public void Dispose() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(true); - // TODO: uncomment the following line if the finalizer is overridden above. - // GC.SuppressFinalize(this); - } - #endregion - } -} diff --git a/TGServerService/LockDependancies.txt b/TGServerService/LockDependancies.txt deleted file mode 100644 index 032a584196..0000000000 --- a/TGServerService/LockDependancies.txt +++ /dev/null @@ -1,12 +0,0 @@ -RepoLock protects the repo for the full duration of short operations -RepoBusy protects the repo for long operations - -lock RepoLock and check RepoBusy to see if you can use the repo - -configLock is only for atomically reading and writing the config directory - -compilerLock protects the compilerCurrentStatus and lastCompilerError vars - -byondLock protects the updateStat and lastError vars - -watchdogLock protects the currentStatus var \ No newline at end of file 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..ecf336a9a0 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 - public static class ProcessExtension + /// + /// Helpers to ing and a . Lightly massaged code from https://stackoverflow.com/a/13109774. Documentation linked from MSDN on 20/10/2017 + /// + 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..a9c208a629 100644 --- a/TGServerService/Program.cs +++ b/TGServerService/Program.cs @@ -1,15 +1,26 @@ -using System; +using System; using System.Collections.Generic; using System.IO; +using System.ServiceProcess; 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() { + using (var S = new Service()) + ServiceBase.Run(S); + } + /// + /// 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 +32,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); @@ -29,11 +46,8 @@ namespace TGServerService if (excludeRoot != null) for (var I = 0; I < excludeRoot.Count; ++I) excludeRoot[I] = excludeRoot[I].ToLower(); - if (!di.Attributes.HasFlag(FileAttributes.Directory)) - { //this is probably a symlink - Directory.Delete(di.FullName); + if (CheckDeleteSymlinkDir(di)) return; - } NormalizeAndDelete(di, excludeRoot); if (!ContentsOnly) { @@ -43,12 +57,35 @@ namespace TGServerService } } + + /// + /// Properly unlinks directory if it is a symlink + /// + /// for the directory in question + /// if was a symlink and deleted, otherwise + static bool CheckDeleteSymlinkDir(DirectoryInfo di) + { + if (!di.Attributes.HasFlag(FileAttributes.Directory)) + { //this is probably a symlink + Directory.Delete(di.FullName); + return true; + } + return false; + } + + /// + /// 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()) { if (excludeRoot != null && excludeRoot.Contains(subDir.Name.ToLower())) continue; + if (CheckDeleteSymlinkDir(subDir)) + continue; NormalizeAndDelete(subDir, null); subDir.Delete(true); } @@ -61,8 +98,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 +150,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 +159,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 deleted file mode 100644 index 1132b0a780..0000000000 --- a/TGServerService/ProjectInstaller.Designer.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace ServerService -{ - partial class ProjectInstaller - { - /// - /// 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() - { - this.serviceProcessInstaller1 = new System.ServiceProcess.ServiceProcessInstaller(); - this.serviceInstaller1 = new System.ServiceProcess.ServiceInstaller(); - // - // serviceProcessInstaller1 - // - this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.LocalSystem; - this.serviceProcessInstaller1.Password = null; - this.serviceProcessInstaller1.Username = null; - // - // serviceInstaller1 - // - this.serviceInstaller1.Description = "/tg/station Server Service"; - this.serviceInstaller1.DisplayName = "TG Station Server"; - this.serviceInstaller1.ServiceName = "TG Station Server"; - this.serviceInstaller1.StartType = System.ServiceProcess.ServiceStartMode.Automatic; - // - // ProjectInstaller - // - this.Installers.AddRange(new System.Configuration.Install.Installer[] { - this.serviceProcessInstaller1, - this.serviceInstaller1}); - - } - - #endregion - - private System.ServiceProcess.ServiceProcessInstaller serviceProcessInstaller1; - private System.ServiceProcess.ServiceInstaller serviceInstaller1; - } -} \ No newline at end of file diff --git a/TGServerService/ProjectInstaller.cs b/TGServerService/ProjectInstaller.cs index 3c405c0328..f5c0f1a911 100644 --- a/TGServerService/ProjectInstaller.cs +++ b/TGServerService/ProjectInstaller.cs @@ -2,14 +2,34 @@ 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 + public sealed class ProjectInstaller : Installer { + /// + /// Construct a + /// public ProjectInstaller() { - InitializeComponent(); + Installers.AddRange(new Installer[] { + new ServiceProcessInstaller + { + Account = ServiceAccount.LocalSystem, + Password = null, + Username = null + }, + new ServiceInstaller + { + Description = "/tg/station Server Service", + DisplayName = "TG Station Server", + ServiceName = "TG Station Server", + StartType = ServiceStartMode.Automatic + } + }); } } } diff --git a/TGServerService/ProjectInstaller.resx b/TGServerService/ProjectInstaller.resx deleted file mode 100644 index 235f1b0bfb..0000000000 --- a/TGServerService/ProjectInstaller.resx +++ /dev/null @@ -1,129 +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 - - - 17, 56 - - - 196, 17 - - - False - - \ No newline at end of file diff --git a/TGServerService/Properties/AssemblyInfo.cs b/TGServerService/Properties/AssemblyInfo.cs index e9ebd6fc8c..6df01aa814 100644 --- a/TGServerService/Properties/AssemblyInfo.cs +++ b/TGServerService/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -14,3 +15,6 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("f32eda25-0855-411c-af5e-f0d042917e2d")] + +//allow the unit tester to peek inside us +[assembly: InternalsVisibleTo("TGServiceTests", AllInternalsVisible = true)] diff --git a/TGServerService/Properties/Settings.Designer.cs b/TGServerService/Properties/Settings.Designer.cs index 2ff095f958..1bbb0d95f0 100644 --- a/TGServerService/Properties/Settings.Designer.cs +++ b/TGServerService/Properties/Settings.Designer.cs @@ -23,102 +23,6 @@ namespace TGServerService.Properties { } } - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("tgstation")] - public string ProjectName { - get { - return ((string)(this["ProjectName"])); - } - set { - this["ProjectName"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("1337")] - public ushort ServerPort { - get { - return ((ushort)(this["ServerPort"])); - } - set { - this["ServerPort"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("False")] - public bool PushChangelogToGit { - get { - return ((bool)(this["PushChangelogToGit"])); - } - set { - this["PushChangelogToGit"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("tgstation-server")] - public string CommitterName { - get { - return ((string)(this["CommitterName"])); - } - set { - this["CommitterName"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("tgstation-server@tgstation13.org")] - public string CommitterEmail { - get { - return ((string)(this["CommitterEmail"])); - } - set { - this["CommitterEmail"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("C:/tgstation-server-3")] - public string ServerDirectory { - get { - return ((string)(this["ServerDirectory"])); - } - set { - this["ServerDirectory"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("0")] - public int ServerSecurity { - get { - return ((int)(this["ServerSecurity"])); - } - set { - this["ServerSecurity"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("False")] - public bool DDAutoStart { - get { - return ((bool)(this["DDAutoStart"])); - } - set { - this["DDAutoStart"] = value; - } - } - [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("C:\\Python27")] @@ -131,30 +35,6 @@ namespace TGServerService.Properties { } } - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("NEEDS INITIALIZING")] - public string ChatProviderData { - get { - return ((string)(this["ChatProviderData"])); - } - set { - this["ChatProviderData"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("")] - public string ChatProviderEntropy { - get { - return ((string)(this["ChatProviderEntropy"])); - } - set { - this["ChatProviderEntropy"] = value; - } - } - [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("True")] @@ -169,43 +49,7 @@ namespace TGServerService.Properties { [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool ReattachToDD { - get { - return ((bool)(this["ReattachToDD"])); - } - set { - this["ReattachToDD"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("0")] - public int ReattachPID { - get { - return ((int)(this["ReattachPID"])); - } - set { - this["ReattachPID"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("0")] - public ushort ReattachPort { - get { - return ((ushort)(this["ReattachPort"])); - } - set { - this["ReattachPort"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("5")] + [global::System.Configuration.DefaultSettingValueAttribute("7")] public int SettingsVersion { get { return ((int)(this["SettingsVersion"])); @@ -215,42 +59,6 @@ namespace TGServerService.Properties { } } - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("")] - public string ReattachCommsKey { - get { - return ((string)(this["ReattachCommsKey"])); - } - set { - this["ReattachCommsKey"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("False")] - public bool Webclient { - get { - return ((bool)(this["Webclient"])); - } - set { - this["Webclient"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("")] - public string AuthorizedGroupSID { - get { - return ((string)(this["AuthorizedGroupSID"])); - } - set { - this["AuthorizedGroupSID"] = value; - } - } - [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("38607")] @@ -265,37 +73,12 @@ namespace TGServerService.Properties { [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("")] - public string ReattachAPIVersion { + public global::System.Collections.Specialized.StringCollection InstancePaths { get { - return ((string)(this["ReattachAPIVersion"])); + return ((global::System.Collections.Specialized.StringCollection)(this["InstancePaths"])); } set { - this["ReattachAPIVersion"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("0")] - public ulong AutoUpdateInterval { - get { - return ((ulong)(this["AutoUpdateInterval"])); - } - set { - this["AutoUpdateInterval"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("False")] - public bool PushTestmergeCommits { - get { - return ((bool)(this["PushTestmergeCommits"])); - } - set { - this["PushTestmergeCommits"] = value; + this["InstancePaths"] = value; } } } diff --git a/TGServerService/Properties/Settings.settings b/TGServerService/Properties/Settings.settings index 6cae9c7ba7..447b6c7ee3 100644 --- a/TGServerService/Properties/Settings.settings +++ b/TGServerService/Properties/Settings.settings @@ -2,74 +2,20 @@ - - tgstation - - - 1337 - - - False - - - tgstation-server - - - tgstation-server@tgstation13.org - - - C:/tgstation-server-3 - - - 0 - - - False - C:\Python27 - - NEEDS INITIALIZING - - - - True - - True - - - 0 - - - 0 - - 5 - - - - - - False - - - + 7 38607 - + - - 0 - - - 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/ServerInstance/Administration.cs b/TGServerService/ServerInstance/Administration.cs new file mode 100644 index 0000000000..87891795af --- /dev/null +++ b/TGServerService/ServerInstance/Administration.cs @@ -0,0 +1,182 @@ +using System; +using System.DirectoryServices.AccountManagement; +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 + 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(); + /// + /// 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; + + /// + public string GetCurrentAuthorizedGroup() + { + try + { + if (TheDroidsWereLookingFor == null) + return "ADMIN"; + + string res = null; + try + { + res = GroupPrincipal.FindByIdentity(new PrincipalContext(ContextType.Machine), IdentityType.Sid, TheDroidsWereLookingFor.Value).Name; + } + catch { } + return res ?? GroupPrincipal.FindByIdentity(new PrincipalContext(ContextType.Domain), IdentityType.Sid, TheDroidsWereLookingFor.Value).Name; + } + catch + { + return null; + } + } + + /// + public string SetAuthorizedGroup(string groupName) + { + if (groupName == null) + { + TheDroidsWereLookingFor = null; + Config.AuthorizedUserGroupSID = null; + Config.Save(); + return "ADMIN"; + } + return FindTheDroidsWereLookingFor(groupName); + } + + /// + /// Set based off either an ed name or a string from the config + /// + /// The name of the group to search for + /// Recursive parameter used to check for the group using instead of + /// The name of the group allowed to access the if it could be found, otherwise + string FindTheDroidsWereLookingFor(string search = null, bool useDomain = false) + { + //find the group that is authorized to use the tools + var pc = new PrincipalContext(useDomain ? ContextType.Domain : ContextType.Machine); + var groupName = search ?? Config.AuthorizedUserGroupSID; + if (String.IsNullOrWhiteSpace(groupName)) + return null; + var gp = GroupPrincipal.FindByIdentity(pc, search != null ? IdentityType.Name : IdentityType.Sid, groupName); + if (gp == null) + { + if (search != null) + //try again with all types + gp = GroupPrincipal.FindByIdentity(pc, search); + if (gp == null) + return useDomain ? null : FindTheDroidsWereLookingFor(search, true); + } + TheDroidsWereLookingFor = gp.Sid; + if (search != null) + { + Config.AuthorizedUserGroupSID = TheDroidsWereLookingFor.Value; + Config.Save(); + } + return gp.Name; + } + + /// + /// 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; + + if (contract == typeof(ITGConnectivity).Name) //always allow connectivity checks + return true; + + 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, and don't spam the logs with it unless it fails + var result = windowsIdent.User == ServiceSID; + if(!result) + WriteAccess(windowsIdent.Name, false); + return result; + } + + var wp = new WindowsPrincipal(windowsIdent); + //first allow admins + var authSuccess = wp.IsInRole(WindowsBuiltInRole.Administrator); + + //if we're not an admin, check that we aren't trying to access the admin interface + if (!authSuccess && operationContext.EndpointDispatcher.ContractName != typeof(ITGAdministration).Name && TheDroidsWereLookingFor != null) + authSuccess = wp.IsInRole(new SecurityIdentifier(Config.AuthorizedUserGroupSID)); + + lock (authLock) + { + var user = windowsIdent.Name; + if (LastSeenUser != user) + { + LastSeenUser = user; + WriteAccess(user, authSuccess); + } + } + return authSuccess; + } + + public string RecreateStaticFolder() + { + if (!Monitor.TryEnter(RepoLock)) + return "Repo locked!"; + try + { + if (!Monitor.TryEnter(watchdogLock)) + return "Watchdog locked!"; + try + { + if (!Monitor.TryEnter(configLock)) + return "Static dir locked!"; + try + { + if (currentStatus != DreamDaemonStatus.Offline) + return "Watchdog running!"; + BackupAndDeleteStaticDirectory(); + InitialConfigureRepository(); + } + finally + { + Monitor.Exit(configLock); + } + } + finally + { + Monitor.Exit(watchdogLock); + } + } + catch(Exception e) + { + return e.ToString(); + } + finally + { + Monitor.Exit(RepoLock); + } + return null; + } + } +} diff --git a/TGServerService/ServerInstance/Byond.cs b/TGServerService/ServerInstance/Byond.cs new file mode 100644 index 0000000000..2b902a52f6 --- /dev/null +++ b/TGServerService/ServerInstance/Byond.cs @@ -0,0 +1,353 @@ +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() + { + var rrdp = RelativePath(RevisionDownloadPath); + //linger not + if (File.Exists(rrdp)) + File.Delete(rrdp); + Program.DeleteDirectory(RelativePath(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 = RelativePath(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]); + var rrdp = RelativePath(RevisionDownloadPath); + 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), rrdp); + } + 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); + 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(rrdp, RelativePath(StagingDirectory)); + lock (ByondLock) + { + File.WriteAllText(RelativePath(StagingDirectoryInner + VersionFile), String.Format("{0}.{1}", major, minor)); + //IMPORTANT: SET THE BYOND CONFIG TO NOT PROMPT FOR TRUSTED MODE REEE + Directory.CreateDirectory(RelativePath(ByondConfigDir)); + File.WriteAllText(RelativePath(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); + WriteInfo(String.Format("BYOND update {0}.{1} staged", major, minor), EventID.BYONDUpdateStaged); + break; + } + } + catch (ThreadAbortException) + { + return; + } + catch (Exception e) + { + 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 + { + var rbd = RelativePath(ByondDirectory); + Program.DeleteDirectory(rbd); + Directory.Move(RelativePath(StagingDirectoryInner), rbd); + Program.DeleteDirectory(RelativePath(StagingDirectory)); + lastError = null; + SendMessage("BYOND: Update completed!", MessageType.DeveloperInfo); + 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); + 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..f81c97c036 --- /dev/null +++ b/TGServerService/ServerInstance/Chat.cs @@ -0,0 +1,242 @@ +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 + { + /// + /// Used for indicating unintialized encrypted data + /// + public const string UninitializedString = "NEEDS INITIALIZING"; + + /// + /// 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: + WriteError(String.Format("Invalid chat provider: {0}", info.Provider), EventID.InvalidChatProvider); + continue; + } + } + catch (Exception e) + { + 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) + 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, + }; + 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 == UninitializedString) + return new List() { new IRCSetupInfo() { Nickname = Config.Name }, 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 = UninitializedString; + } + } + //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) + { + 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 58% rename from TGServerService/Compiler.cs rename to TGServerService/ServerInstance/Compiler.cs index 71c78187c4..e7432e0d17 100644 --- a/TGServerService/Compiler.cs +++ b/TGServerService/ServerInstance/Compiler.cs @@ -1,633 +1,611 @@ -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 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; + + 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() + { + var rldt = RelativePath(LiveDirTest); + if (File.Exists(rldt)) + File.Delete(rldt); + 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 + static 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(RelativePath(Path.Combine(GameDirLive, BridgeDLLName)))) //its a good tell, jim + return CompilerStatus.Initialized; + return CompilerStatus.Uninitialized; + } + + //we need to remove symlinks before we can recursively delete + void CleanGameFolder() + { + var GameDirABridge = RelativePath(Path.Combine(GameDirA, BridgeDLLName)); + if (Directory.Exists(GameDirABridge)) + Directory.Delete(GameDirABridge); + + var GameDirBBridge = RelativePath(Path.Combine(GameDirB, BridgeDLLName)); + if (Directory.Exists(GameDirBBridge)) + Directory.Delete(GameDirBBridge); + + var rgdl = RelativePath(GameDirLive); + if (Directory.Exists(rgdl)) + Directory.Delete(rgdl); + } + + //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(RelativePath(GameDir)); + + Directory.CreateDirectory(RelativePath(GameDirA)); + Directory.CreateDirectory(RelativePath(GameDirB)); + + var rep_config = GetCachedRepoConfig(); + + if (rep_config != null) { + foreach (var I in rep_config.StaticDirectoryPaths) + CreateSymlink(RelativePath(Path.Combine(GameDirA, I)), RelativePath(Path.Combine(StaticDirs, I))); + foreach (var I in rep_config.DLLPaths) + CreateSymlink(RelativePath(Path.Combine(GameDirA, I)), RelativePath(Path.Combine(StaticDirs, I))); + } + + var rbdlln = RelativePath(BridgeDLLName); + CreateSymlink(RelativePath(Path.Combine(GameDirA, BridgeDLLName)), rbdlln); + CreateSymlink(RelativePath(Path.Combine(GameDirB, BridgeDLLName)), rbdlln); + + CreateSymlink(RelativePath(GameDirLive), RelativePath(GameDirA)); + + lock (CompilerLock) + { + compilerCurrentStatus = CompilerStatus.Compiling; + silentCompile = true; + } + } + catch (ThreadAbortException) + { + return; + } + catch (Exception e) + { + lock (CompilerLock) + { + SendMessage("DM: Setup failed!", MessageType.DeveloperInfo); + 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(RelativePath(GameDirLive))) + TheDir = GameDirA; + else + { + File.Create(RelativePath(LiveDirTest)).Close(); + try + { + if (File.Exists(RelativePath(ADirTest))) + TheDir = GameDirA; + else if (File.Exists(RelativePath(BDirTest))) + TheDir = GameDirB; + else + throw new Exception("Unable to determine current live directory!"); + } + finally + { + File.Delete(RelativePath(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 = RelativePath(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(RelativePath(GameDirLive)); + CreateSymlink(RelativePath(GameDirLive), TheDir); + return RelativePath(InvertDirectory(TheDir)); + } + } + return RelativePath(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(); //non-relative + + var Config = GetCachedRepoConfig(); + var deleteExcludeList = new List { BridgeDLLName }; + 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), RelativePath(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, RelativePath(Path.Combine(StaticDirs, I))); + } + + if (!File.Exists(Path.Combine(resurrectee, BridgeDLLName))) + CreateSymlink(Path.Combine(resurrectee, BridgeDLLName), RelativePath(BridgeDLLName)); + + deleteExcludeList.Add(".git"); + Program.CopyDirectory(RelativePath(RepoPath), resurrectee, deleteExcludeList); + CurrentSha = GetHead(false, out string error); + //just the tip + const string GitLogsDir = "/.git/logs"; + Program.CopyDirectory(RelativePath(RepoPath + GitLogsDir), resurrectee + GitLogsDir); + try + { + File.Copy(RelativePath(PRJobFile), Path.Combine(resurrectee, 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); + 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 + WriteWarning("Precompile hook failed!", EventID.DMCompileError); + return; + } + + using (var DM = new Process()) //will kill the process if the thread is terminated + { + DM.StartInfo.FileName = RelativePath(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) + { + //gotta go fast + var online = currentStatus == DreamDaemonStatus.Online; + if (online) + Proc.Suspend(); + try + { + var rgdl = RelativePath(GameDirLive); + if (Directory.Exists(rgdl)) + //these next two lines should be atomic but this is the best we can do + Directory.Delete(rgdl); + CreateSymlink(rgdl, resurrectee); + } + finally + { + if (online && !Proc.HasExited) + Proc.Resume(); + } + } + var staged = DaemonStatus() != DreamDaemonStatus.Offline; + if (!PostcompileHook()) + { + lastCompilerError = "The postcompile hook failed"; + compilerCurrentStatus = CompilerStatus.Initialized; //still fairly valid + 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); + 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 + 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); + 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); + 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 70% rename from TGServerService/Config.cs rename to TGServerService/ServerInstance/Config.cs index 188a2c838d..d59fef5909 100644 --- a/TGServerService/Config.cs +++ b/TGServerService/ServerInstance/Config.cs @@ -1,39 +1,36 @@ 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 configDir = RelativePath(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"; @@ -43,7 +40,7 @@ namespace TGServerService var Found = false; foreach (var I in Config.StaticDirectoryPaths) { - if (di1.FullName == new DirectoryInfo(Path.Combine(RepoPath, I)).FullName) + if (di1.FullName == new DirectoryInfo(Path.Combine(RelativePath(RepoPath), I)).FullName) { Found = true; break; @@ -78,8 +75,8 @@ namespace TGServerService } var output = File.ReadAllText(path); - TGServerService.CancelImpersonation(); - TGServerService.WriteInfo("Read of " + path, TGServerService.EventID.StaticRead); + Service.CancelImpersonation(); + WriteInfo("Read of " + path, EventID.StaticRead); error = null; unauthorized = false; return output; @@ -95,19 +92,23 @@ namespace TGServerService catch (Exception e) { error = e.ToString(); + Service.CancelImpersonation(); + 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 = RelativePath(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); + var di1 = new DirectoryInfo(RelativePath(StaticDirs)); var destdir = new FileInfo(path).Directory.FullName; var di2 = new DirectoryInfo(destdir); @@ -130,8 +131,8 @@ namespace TGServerService Directory.CreateDirectory(destdir); File.WriteAllText(path, data); - TGServerService.CancelImpersonation(); - TGServerService.WriteInfo("Write to " + path, TGServerService.EventID.StaticWrite); + Service.CancelImpersonation(); + WriteInfo("Write to " + path, EventID.StaticWrite); unauthorized = false; return null; } @@ -145,18 +146,21 @@ namespace TGServerService catch (Exception e) { unauthorized = false; + Service.CancelImpersonation(); + 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 = RelativePath(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); + var di1 = new DirectoryInfo(RelativePath(StaticDirs)); var fi = new FileInfo(path); var di2 = new DirectoryInfo(fi.Directory.FullName); @@ -181,8 +185,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(); + WriteInfo("Delete of " + path, EventID.StaticDelete); unauthorized = false; return null; } @@ -196,22 +200,25 @@ namespace TGServerService catch (Exception e) { unauthorized = false; + Service.CancelImpersonation(); + 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) { try { - if (!Directory.Exists(StaticDirs)) + if (!Directory.Exists(RelativePath(StaticDirs))) { error = null; unauthorized = false; return new List(); } - DirectoryInfo dirToEnum = new DirectoryInfo(StaticDirs + '/' + subDir ?? ""); //do not use path.combine or it will try and take the root + DirectoryInfo dirToEnum = new DirectoryInfo(RelativePath(StaticDirs) + '/' + subDir ?? ""); //do not use path.combine or it will try and take the root var result = new List(); foreach (var I in dirToEnum.GetFiles()) result.Add(I.Name); diff --git a/TGServerService/ServerInstance/DreamDaemon.cs b/TGServerService/ServerInstance/DreamDaemon.cs new file mode 100644 index 0000000000..51c7acecb2 --- /dev/null +++ b/TGServerService/ServerInstance/DreamDaemon.cs @@ -0,0 +1,750 @@ +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 of , and create the + /// + void InitDreamDaemon() + { + Directory.CreateDirectory(RelativePath(DiagnosticsDir)); + Directory.CreateDirectory(RelativePath(ResourceDiagnosticsDir)); + var Reattach = Config.ReattachRequired; + if (Reattach) + try + { + Proc = Process.GetProcessById(Config.ReattachProcessID); + if (Proc == null) + throw new Exception("GetProcessById returned null!"); + 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) + { + 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 = RelativePath(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 of + /// + 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 + { + var rrdd = RelativePath(ResourceDiagnosticsDir); + File.Move(Path.Combine(rrdd, CurrentDDLog), Path.Combine(rrdd, "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(RelativePath(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); + WriteInfo("Watchdog started", EventID.DDWatchdogStarted); + } + else + { + RestartInProgress = false; + if (!ReattachInsteadOfRestart) + 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); + 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); + 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); + WriteInfo("Watch dog exited", EventID.DDWatchdogExit); + } + else + 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 = RelativePath(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 of 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 UpdateBridgeDll(bool overwrite) + { + var rbdlln = RelativePath(BridgeDLLName); + var FileExists = File.Exists(rbdlln); + if (FileExists && !overwrite) + return; + //Copy the interface dll to the static dir + + var InterfacePath = Assembly.GetAssembly(typeof(Interface)).Location; + //bridge is installed next to the interface + var BridgePath = Path.Combine(Path.GetDirectoryName(InterfacePath), BridgeDLLName); +#if DEBUG + //We could be debugging from the project directory + if (!File.Exists(BridgePath)) + //A little hackish debug mode doctoring never hurt anyone + BridgePath = Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(InterfacePath)))), "TGDreamDaemonBridge/bin/x86/Debug", BridgeDLLName); +#endif + try + { + //Use reflection to ensure these are the droids we're looking for + Assembly.ReflectionOnlyLoadFrom(BridgePath).GetType(DreamDaemonBridgeType, true); + } + catch (Exception e) + { + WriteError(String.Format("Unable to locate {0}! Error: {1}", BridgeDLLName, e.ToString()), EventID.BridgeDLLUpdateFail); + return; + } + + try + { + if (FileExists) + { + var Old = File.ReadAllBytes(rbdlln); + var New = File.ReadAllBytes(BridgePath); + if (Old.SequenceEqual(New)) + return; //no need + } + File.Copy(BridgePath, rbdlln, overwrite); + } + catch + { + try + { + //ok the things being stupid and hasn't released the dll yet, try ONCE more + Thread.Sleep(1000); + File.Copy(BridgePath, rbdlln, overwrite); + } + catch (Exception e) + { + //intentionally using the fi + WriteError("Failed to update bridge DLL! Error: " + e.ToString(), EventID.BridgeDLLUpdateFail); + return; + } + } + WriteInfo("Updated interface DLL", EventID.BridgeDLLUpdated); + } + + /// + /// 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 = RelativePath(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}&{6}={7}\" -{2} -public", DMB, Config.Port, SecurityWord(), serviceCommsKey, Version(), Config.Webclient ? "-webclient " : "", SPInstanceName, Config.Name); + UpdateBridgeDll(true); + lock (topicLock) + { + GameAPIVersion = null; //needs updating + } + Proc.Start(); + Proc.PriorityClass = ProcessPriorityClass.AboveNormal; + + 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 68% rename from TGServerService/Interop.cs rename to TGServerService/ServerInstance/Interop.cs index 42ea994f50..ba64d4a372 100644 --- a/TGServerService/Interop.cs +++ b/TGServerService/ServerInstance/Interop.cs @@ -1,253 +1,269 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Web.Script.Serialization; -using System.Web.Security; -using TGServiceInterface; - -namespace TGServerService -{ - //handles talking between the world and us - partial class TGStationServer : ITGInterop - { - - object topicLock = new object(); - const int CommsKeyLen = 64; - string serviceCommsKey; //regenerated every DD restart - - //range of supported api versions - readonly Version MinAPIVersion = new Version("3.1.0.0"); - readonly Version MaxAPIVersion = new Version("3.1.0.99"); - Version GameAPIVersion; - - //See code/modules/server_tools/server_tools.dm for command switch - const string SCHardReboot = "hard_reboot"; //requests that dreamdaemon restarts when the round ends - const string SCGracefulShutdown = "graceful_shutdown"; //requests that dreamdaemon stops when the round ends - const string SCWorldAnnounce = "world_announce"; //sends param 'message' to the world - const string SCListCustomCommands = "list_custom_commands"; //Get a list of commands supported by the server - const string SCAPICompat = "api_compat"; //Tells the server we understand each other - const string SCPlayerCount = "client_count"; //Gets the number of connected clients - - const string SRKillProcess = "killme"; - const string SRIRCBroadcast = "irc"; - const string SRIRCAdminChannelMessage = "send2irc"; - const string SRWorldReboot = "worldreboot"; - const string SRAPIVersion = "api_ver"; - - const string CCPHelpText = "help_text"; - const string CCPAdminOnly = "admin_only"; - const string CCPRequiredParameters = "required_parameters"; - - List ServerChatCommands; - - void LoadServerChatCommands() - { - if (DaemonStatus() != TGDreamDaemonStatus.Online) - return; - var json = SendCommand(SCListCustomCommands); - if (String.IsNullOrWhiteSpace(json)) - return; - List tmp = new List(); - try - { - foreach(var I in new JavaScriptSerializer().Deserialize>>(json)) - tmp.Add(new ServerChatCommand(I.Key, (string)I.Value[CCPHelpText], ((int)I.Value[CCPAdminOnly]) == 1, (int)I.Value[CCPRequiredParameters])); - ServerChatCommands = tmp; - } - catch { } - } - - //raw command string sent here via world.ExportService - void HandleCommand(string cmd) - { - var splits = new List(cmd.Split(' ')); - cmd = splits[0]; - splits.RemoveAt(0); - - bool APIValid; - lock (topicLock) - { - APIValid = CheckAPIVersionConstraints(); - } - - if (!APIValid && cmd != SRAPIVersion) - return; //SPEAK THE LANGUAGE!!! - - switch (cmd) - { - case SRIRCBroadcast: - SendMessage("GAME: " + String.Join(" ", splits), ChatMessageType.GameInfo); - break; - case SRKillProcess: - KillMe(); - break; - case SRIRCAdminChannelMessage: - SendMessage("RELAY: " + String.Join(" ", splits), ChatMessageType.AdminInfo); - break; - case SRWorldReboot: - TGServerService.WriteInfo("World Rebooted", TGServerService.EventID.WorldReboot); - WriteCurrentDDLog("World rebooted"); - ServerChatCommands = null; - ChatConnectivityCheck(); - lock (CompilerLock) - { - if (UpdateStaged) - { - UpdateStaged = false; - lock (topicLock) - { - GameAPIVersion = null; //needs updating - } - TGServerService.WriteInfo("Staged update applied", TGServerService.EventID.ServerUpdateApplied); - } - } - break; - case SRAPIVersion: - lock (topicLock) - { - try - { - GameAPIVersion = new Version(splits[0]); - if (!CheckAPIVersionConstraints()) - throw new Exception(); - } - catch - { - TGServerService.WriteWarning(String.Format("API version of the game ({0}) is incompatible with the current supported API versions (Min: {1}. Max: {2}). Interop disabled.", splits.Count > 1 ? splits[1] : "NULL", MinAPIVersion, MaxAPIVersion), TGServerService.EventID.APIVersionMismatch); - GameAPIVersion = null; - break; - } - } - //This needs to be done asyncronously otherwise DD won't be able to process it, because it's waiting for THIS THREAD to return - ThreadPool.QueueUserWorkItem(_ => SendCommand(SCAPICompat)); - break; - } - } - - public string SendCommand(string cmd) - { - lock (watchdogLock) - { - if (currentStatus != TGDreamDaemonStatus.Online) - return "Error: Server Offline!"; - return SendTopic(String.Format("serviceCommsKey={0};command={1}", serviceCommsKey, cmd), currentPort); - } - } - - public int PlayerCount() - { - try - { - return Convert.ToInt32(SendCommand(SCPlayerCount)); - } - catch - { - return -1; - } - } - - //requires topiclock - bool CheckAPIVersionConstraints() - { - return !(GameAPIVersion == null || GameAPIVersion < MinAPIVersion || GameAPIVersion > MaxAPIVersion); - } - - public static string SanitizeTopicString(string input) - { - return input.Replace("%", "%25").Replace("=", "%3d").Replace(";", "%3b").Replace("&", "%26").Replace("+", "%2b"); - } - - //Fuckery to diddle byond with the right packet to accept our girth - string SendTopic(string topicdata, ushort port) - { - //santize the escape characters in accordance with http://www.byond.com/docs/ref/info.html#/proc/params2list - lock (topicLock) { - if (!CheckAPIVersionConstraints()) - return "Incompatible API!"; - using (var topicSender = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { SendTimeout = 5000, ReceiveTimeout = 5000 }) - { - try - { - topicSender.Connect(IPAddress.Loopback, port); - - StringBuilder stringPacket = new StringBuilder(); - stringPacket.Append((char)'\x00', 8); - stringPacket.Append('?' + topicdata); - stringPacket.Append((char)'\x00'); - string fullString = stringPacket.ToString(); - var packet = Encoding.ASCII.GetBytes(fullString); - packet[1] = 0x83; - var FinalLength = packet.Length - 4; - if (FinalLength > UInt16.MaxValue) - return "Error: Topic too long"; - - var lengthBytes = BitConverter.GetBytes((ushort)FinalLength); - - packet[2] = lengthBytes[1]; //fucking endianess - packet[3] = lengthBytes[0]; - - topicSender.Send(packet); - - string returnedString = "NULL"; - try - { - var returnedData = new byte[UInt16.MaxValue]; - topicSender.Receive(returnedData); - var raw_string = Encoding.ASCII.GetString(returnedData).TrimEnd(new char[] { (char)0 }).Trim(); - if (raw_string.Length > 6) - returnedString = raw_string.Substring(5, raw_string.Length - 5).Trim(); - } - catch - { - returnedString = "Topic recieve error!"; - } - finally - { - topicSender.Shutdown(SocketShutdown.Both); - } - - return returnedString; - } - catch - { - return "Topic delivery failed!"; - } - } - } - } - - //Every time we make a new DD process we generate a new comms key for security - //It's in world.params['server_service'] - void GenCommsKey() - { - var charsToRemove = new string[] { "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "-", "+", "=", "[", "{", "]", "}", ";", ":", "<", ">", "|", ".", "/", "?" }; - serviceCommsKey = String.Empty; - do { - var tmp = Membership.GeneratePassword(CommsKeyLen, 0); - foreach (var c in charsToRemove) - tmp = tmp.Replace(c, String.Empty); - serviceCommsKey += tmp; - } while (serviceCommsKey.Length < CommsKeyLen); - serviceCommsKey = serviceCommsKey.Substring(0, CommsKeyLen); - TGServerService.WriteInfo("Service Comms Key set to: " + serviceCommsKey, TGServerService.EventID.CommsKeySet); - } - - /// - public bool InteropMessage(string command) - { - try - { - HandleCommand(command); - return true; - } - catch(Exception e) - { - TGServerService.WriteWarning(String.Format("Handle command for \"{0}\" failed: {1}", command, e.ToString()), TGServerService.EventID.InteropCallException); - return false; - } - } - } -} +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Web.Script.Serialization; +using System.Web.Security; +using TGServerService.ChatCommands; +using TGServiceInterface; +using TGServiceInterface.Components; + +namespace TGServerService +{ + //handles talking between the world and us + 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 = 2; + Version GameAPIVersion; + + const string SPInstanceName = "server_instance"; + + //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 client + + /// + /// String returned when a command completes successfully with no output + /// + const string SRetSuccess = "SUCCESS"; + + 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"; + + /// + /// The file name of the .dll that contains the bridge class + /// + const string BridgeDLLName = "TGDreamDaemonBridge.dll"; + /// + /// The namespace that contains the bridge class. Used for reflection + /// + const string DreamDaemonBridgeNamespace = "TGDreamDaemonBridge"; + /// + /// The bridge class. Used for reflection + /// + const string DreamDaemonBridgeType = DreamDaemonBridgeNamespace + ".DreamDaemonBridge"; + + 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: + WriteInfo("World Rebooted", EventID.WorldReboot); + WriteCurrentDDLog("World rebooted"); + ServerChatCommands = null; + ChatConnectivityCheck(); + lock (CompilerLock) + { + if (UpdateStaged) + { + UpdateStaged = false; + lock (topicLock) + { + GameAPIVersion = null; //needs updating + } + WriteInfo("Staged update applied", EventID.ServerUpdateApplied); + } + } + break; + case SRAPIVersion: + lock (topicLock) + { + try + { + GameAPIVersion = new Version(splits[0]); + if (!CheckAPIVersionConstraints()) + throw new Exception(); + } + catch + { + 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!"; + + 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]; + + var returnedString = "NULL"; + var returnedData = new byte[UInt16.MaxValue]; + using (var topicSender = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { SendTimeout = 5000, ReceiveTimeout = 5000 }) + try + { + topicSender.Connect(IPAddress.Loopback, port); + topicSender.Send(packet); + + try + { + 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); + } + } + catch + { + return "Topic delivery failed!"; + } + + return returnedString; + } + } + + //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); + WriteInfo("Service Comms Key set to: " + serviceCommsKey, EventID.CommsKeySet); + } + + /// + public bool InteropMessage(string command) + { + try + { + HandleCommand(command); + return true; + } + catch(Exception e) + { + 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..84bc7d8ff9 --- /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(RelativePath(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", RelativePath(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) + WriteInfo(eventData, EventID.PreactionEvent); + else + 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 56% rename from TGServerService/Repository.cs rename to TGServerService/ServerInstance/Repository.cs index 4ca73f7fcb..ecaf06020f 100644 --- a/TGServerService/Repository.cs +++ b/TGServerService/ServerInstance/Repository.cs @@ -1,1299 +1,1364 @@ -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 RemoteTempBranchName = "___TGS3TempBranch"; - const string RepoTGS3SettingsPath = RepoPath + "/TGS3.json"; - const string CachedTGS3SettingsPath = "TGS3.json"; - const string RepoErrorUpToDate = "Already up to date!"; - const string SSHPushRemote = "ssh_push_target"; - const string RepoKeyDir = "RepoKey/"; - const string PrivateKeyPath = RepoKeyDir + "private_key.txt"; - const string PublicKeyPath = RepoKeyDir + "public_key.txt"; - const string PRJobFile = "prtestjob.json"; - const string LiveTrackingBranch = "___TGSLiveCommitTrackingBranch"; - const string CommitMessage = "Automatic changelog compile, [ci skip]"; - - object RepoLock = new object(); - bool RepoBusy = false; - bool Cloning = false; - - Repository Repo; - int currentProgress = -1; - - System.Timers.Timer autoUpdateTimer = new System.Timers.Timer() - { - AutoReset = true - }; - - /// - /// Repo specific information about the installation - /// Requires RepoLock and !RepoBusy to be instantiated - /// - class RepoConfig : IEquatable - { - public readonly bool ChangelogSupport; - public readonly string PathToChangelogPy; - public readonly string ChangelogPyArguments; - public readonly IList PipDependancies = new List(); - public readonly IList PathsToStage = new List(); - public readonly IList StaticDirectoryPaths = new List(); - public readonly IList DLLPaths = new List(); - - public RepoConfig(bool FromRepository) - { - var path = FromRepository ? RepoTGS3SettingsPath : CachedTGS3SettingsPath; - if (!File.Exists(path)) - return; - var rawdata = File.ReadAllText(path); - var Deserializer = new JavaScriptSerializer(); - var json = Deserializer.Deserialize>(rawdata); - try - { - var details = (IDictionary)json["changelog"]; - PathToChangelogPy = (string)details["script"]; - ChangelogPyArguments = (string)details["arguments"]; - ChangelogSupport = true; - try - { - PipDependancies = LoadArray(details["pip_dependancies"]); - } - catch { } - } - catch { - ChangelogSupport = false; - } - try - { - PathsToStage = LoadArray(json["synchronize_paths"]); - } - catch { } - try - { - StaticDirectoryPaths = LoadArray(json["static_directories"]); - } - catch { } - try - { - DLLPaths = LoadArray(json["dlls"]); - } - catch { } - } - private static IList LoadArray(object o) - { - var array = (object[])o; - var res = new List(); - foreach (var I in array) - res.Add((string)I); - return res; - } - - public override bool Equals(object obj) - { - return Equals(obj as RepoConfig); - } - - private static bool ListEquals(IList A, IList B) - { - return A.All(B.Contains) && A.Count == B.Count; - } - - public bool Equals(RepoConfig other) - { - return ChangelogSupport == other.ChangelogSupport - && PathToChangelogPy == other.PathToChangelogPy - && ChangelogPyArguments == other.ChangelogPyArguments - && ListEquals(PipDependancies, other.PipDependancies) - && ListEquals(PathsToStage, other.PathsToStage) - && ListEquals(StaticDirectoryPaths, other.StaticDirectoryPaths) - && ListEquals(DLLPaths, other.DLLPaths); - } - - public override int GetHashCode() - { - var hashCode = 1890628544; - hashCode = hashCode * -1521134295 + ChangelogSupport.GetHashCode(); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(PathToChangelogPy); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ChangelogPyArguments); - hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(PipDependancies); - hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(PathsToStage); - hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(StaticDirectoryPaths); - hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(DLLPaths); - return hashCode; - } - - public static bool operator ==(RepoConfig config1, RepoConfig config2) - { - return EqualityComparer.Default.Equals(config1, config2); - } - - public static bool operator !=(RepoConfig config1, RepoConfig config2) - { - return !(config1 == config2); - } - } - - void InitRepo() - { - Directory.CreateDirectory(RepoKeyDir); - if(Exists()) - UpdateInterfaceDll(false); - if(LoadRepo() == null) - DisableGarbageCollectionNoLock(); - //start the autoupdate timer - autoUpdateTimer.Elapsed += AutoUpdateTimer_Elapsed; - SetAutoUpdateInterval(Properties.Settings.Default.AutoUpdateInterval); - } - - bool RepoConfigsMatch() - { - //this should never be called while the repo is busy - RepoConfig I = null; - lock (RepoLock) - { - if (!RepoBusy && LoadRepo() == null) - I = new RepoConfig(true); - } - if (I == null) - throw new Exception("Unable to load TGS3.json from repo!"); - var J = new RepoConfig(false); - return I == J; - } - - //public api - public bool OperationInProgress() - { - lock (RepoLock) - { - return RepoBusy; - } - } - - //public api - public int CheckoutProgress() - { - return currentProgress; - } - - //Sets up the repo object - string LoadRepo() - { - if (Repo != null) - return null; - if (!Repository.IsValid(RepoPath)) - return "Repository does not exist"; - try - { - Repo = new Repository(RepoPath); - } - catch (Exception e) - { - return e.ToString(); - } - return null; - } - - //Cleans up the repo object - void DisposeRepo() - { - if (Repo != null) - { - Repo.Dispose(); - Repo = null; - } - } - - //public api - public bool Exists() - { - lock (RepoLock) - { - return !Cloning && Repository.IsValid(RepoPath); - } - } - - //Updates the currentProgress var - //no locks required because who gives a shit, it's a fucking 32-bit integer - bool HandleTransferProgress(TransferProgress progress) - { - currentProgress = (int)(((float)progress.ReceivedObjects / progress.TotalObjects) * 100) / 2; - currentProgress += (int)(((float)progress.IndexedObjects / progress.TotalObjects) * 100) / 2; - return true; - } - - //see above - void HandleCheckoutProgress(string path, int completedSteps, int totalSteps) - { - currentProgress = (int)(((float)completedSteps / totalSteps) * 100); - } - - //For the thread parameter - private class TwoStrings - { - public string a, b; - } - - //This is the thread that resets za warldo - //clones, checksout, sets up static dir - void Clone(object twostrings) - { - //busy flag set by caller - var ts = (TwoStrings)twostrings; - var RepoURL = ts.a; - var BranchName = ts.b; - try - { - SendMessage(String.Format("REPO: {2} started: Cloning {0} branch of {1} ...", BranchName, RepoURL, Repository.IsValid(RepoPath) ? "Full reset" : "Setup"), ChatMessageType.DeveloperInfo); - try - { - DisposeRepo(); - Program.DeleteDirectory(RepoPath); - DeletePRList(); - lock (configLock) - { - BackupAndDeleteStaticDirectory(); - } - - var Opts = new CloneOptions() - { - BranchName = BranchName, - RecurseSubmodules = true, - OnTransferProgress = HandleTransferProgress, - OnCheckoutProgress = HandleCheckoutProgress, - CredentialsProvider = GenerateGitCredentials, - }; - - Repository.Clone(RepoURL, RepoPath, Opts); - currentProgress = -1; - LoadRepo(); - - DisableGarbageCollectionNoLock(); - - //create an ssh remote for pushing - Repo.Network.Remotes.Add(SSHPushRemote, RepoURL.Replace("git://", "ssh://").Replace("https://", "ssh://")); - - InitialConfigureRepository(); - - SendMessage("REPO: Clone complete!", ChatMessageType.DeveloperInfo); - TGServerService.WriteInfo("Repository {0}:{1} successfully cloned", TGServerService.EventID.RepoClone); - } - finally - { - currentProgress = -1; - } - } - catch (Exception e) - - { - SendMessage("REPO: Setup failed!", ChatMessageType.DeveloperInfo); - TGServerService.WriteWarning(String.Format("Failed to clone {2}:{0}: {1}", BranchName, e.ToString(), RepoURL), TGServerService.EventID.RepoCloneFail); - } - finally - { - lock (RepoLock) - { - RepoBusy = false; - Cloning = false; - } - } - } - - void DisableGarbageCollectionNoLock() - { - Repo.Config.Set("gc.auto", false); - } - - void BackupAndDeleteStaticDirectory() - { - if (Directory.Exists(StaticDirs)) - { - int count = 1; - - string path = Path.GetDirectoryName(StaticBackupDir); - string newFullPath = StaticBackupDir; - - while (File.Exists(newFullPath) || Directory.Exists(newFullPath)) - { - string tempDirName = string.Format("{0}({1})", StaticBackupDir, count++); - newFullPath = Path.Combine(path, tempDirName); - } - - Program.CopyDirectory(StaticDirs, newFullPath); - } - Program.DeleteDirectory(StaticDirs); - } - - public string UpdateTGS3Json() - { - try - { - if (File.Exists(RepoTGS3SettingsPath)) - File.Copy(RepoTGS3SettingsPath, CachedTGS3SettingsPath, true); - else if (File.Exists(CachedTGS3SettingsPath)) - File.Delete(CachedTGS3SettingsPath); - } - catch(Exception e) - { - return e.ToString(); - } - return null; - } - - void InitialConfigureRepository() - { - Directory.CreateDirectory(StaticDirs); - UpdateInterfaceDll(false); - UpdateTGS3Json(); - var Config = new RepoConfig(false); //RepoBusy is set if we're here - foreach(var I in Config.StaticDirectoryPaths) - { - try - { - var source = Path.Combine(RepoPath, I); - var dest = Path.Combine(StaticDirs, I); - if (Directory.Exists(source)) - Program.CopyDirectory(source, dest); - else - Directory.CreateDirectory(dest); - } - catch - { - TGServerService.WriteWarning("Could not setup static directory: " + I, TGServerService.EventID.RepoConfigurationFail); - } - } - foreach(var I in Config.DLLPaths) - { - try - { - var source = Path.Combine(RepoPath, I); - if (!File.Exists(source)) - { - TGServerService.WriteWarning("Could not find DLL: " + I, TGServerService.EventID.RepoConfigurationFail); - continue; - } - var dest = Path.Combine(StaticDirs, I); - Program.CopyFileForceDirectories(source, dest, false); - } - catch - { - TGServerService.WriteWarning("Could not setup static DLL: " + I, TGServerService.EventID.RepoConfigurationFail); - } - } - } - - //kicks off the cloning thread - //public api - public string Setup(string RepoURL, string BranchName) - { - lock (RepoLock) - { - if (RepoBusy) - return "Repo is busy!"; - lock (CompilerLock) - { - if (!CompilerIdleNoLock()) - return "Compiler is running!"; - } - if (DaemonStatus() != TGDreamDaemonStatus.Offline) - return "DreamDaemon is running!"; - if (RepoURL.Contains("ssh://") && !SSHAuth()) - return String.Format("SSH url specified but either {0} or {1} does not exist in the server directory!", PrivateKeyPath, PublicKeyPath); - RepoBusy = true; - Cloning = true; - new Thread(new ParameterizedThreadStart(Clone)) - { - IsBackground = true //make sure we don't hold up shutdown - }.Start(new TwoStrings { a = RepoURL, b = BranchName }); - return null; - } - } - - //Gets what HEAD is pointing to - string GetShaOrBranch(out string error, bool branch, bool tracked) - { - lock (RepoLock) - { - var result = LoadRepo(); - if (result != null) - { - error = result; - return null; - } - - try - { - error = null; - if (tracked && Repo.Head.TrackedBranch != null) - return Repo.Head.TrackedBranch.Tip.Sha; - return branch ? Repo.Head.FriendlyName : Repo.Head.Tip.Sha; - } - catch (Exception e) - { - error = e.ToString(); - return null; - } - } - } - - //moist shleppy noises - //public api - public string GetHead(bool useTracked, out string error) - { - return GetShaOrBranch(out error, false, useTracked); - } - - //public api - public string GetBranch(out string error) - { - return GetShaOrBranch(out error, true, false); - } - - //public api - public string GetRemote(out string error) - { - try - { - var res = LoadRepo(); - if (res != null) - { - error = res; - return null; - } - error = null; - return Repo.Network.Remotes["origin"].Url; - } - catch (Exception e) - { - error = e.ToString(); - return null; - } - } - - //calls git reset --hard on HEAD - //requires RepoLock - string ResetNoLock(Branch targetBranch) - { - try - { - if (targetBranch != null) - Repo.Reset(ResetMode.Hard, targetBranch.Tip); - else - Repo.Reset(ResetMode.Hard); - return null; - } - catch (Exception e) - { - return e.ToString(); - } - } - - //public api - public string Checkout(string sha) - { - if (sha == LiveTrackingBranch) - return "I'm sorry Dave, I'm afraid I can't do that..."; - lock (RepoLock) - { - var result = LoadRepo(); - if (result != null) - return result; - SendMessage("REPO: Checking out object: " + sha, ChatMessageType.DeveloperInfo); - try - { - if (Repo.Branches[sha] == null) - { - //see if origin has the branch - result = Fetch(); - var trackedBranch = Repo.Branches[String.Format("origin/{0}", sha)]; - if (trackedBranch != null) - { - var newBranch = Repo.CreateBranch(sha, trackedBranch.Tip); - //track it - Repo.Branches.Update(newBranch, b => b.TrackedBranch = trackedBranch.CanonicalName); - } - else if (result != null) - return result; - } - var Opts = new CheckoutOptions() - { - CheckoutModifiers = CheckoutModifiers.Force, - OnCheckoutProgress = HandleCheckoutProgress, - }; - Commands.Checkout(Repo, sha, Opts); - var res = ResetNoLock(null); - UpdateSubmodules(); - SendMessage("REPO: Checkout complete!", ChatMessageType.DeveloperInfo); - TGServerService.WriteInfo("Repo checked out " + sha, TGServerService.EventID.RepoCheckout); - return res; - } - catch (Exception e) - { - SendMessage("REPO: Checkout failed!", ChatMessageType.DeveloperInfo); - TGServerService.WriteWarning(String.Format("Repo checkout of {0} failed: {1}", sha, e.ToString()), TGServerService.EventID.RepoCheckoutFail); - return e.ToString(); - } - } - } - - //Merges a thing into HEAD, not even necessarily a branch - string MergeBranch(string branchname) - { - var mo = new MergeOptions() - { - OnCheckoutProgress = HandleCheckoutProgress - }; - var Result = Repo.Merge(branchname, MakeSig()); - currentProgress = -1; - switch (Result.Status) - { - case MergeStatus.Conflicts: - ResetNoLock(null); - SendMessage("REPO: Merge conflicted, aborted.", ChatMessageType.DeveloperInfo); - return "Merge conflict occurred."; - case MergeStatus.UpToDate: - return RepoErrorUpToDate; - } - return null; - } - - void PushTestmergeCommit() - { - if (Properties.Settings.Default.PushTestmergeCommits && SSHAuth()) - { - try - { - //now try and push the commit to the remote so they can be referenced - var NewB = Repo.CreateBranch(RemoteTempBranchName).CanonicalName; - - var options = new PushOptions() - { - CredentialsProvider = GenerateGitCredentials - }; - var targetRemote = Repo.Network.Remotes[SSHPushRemote]; - Repo.Network.Push(targetRemote, NewB, options); //push the branch - Repo.Network.Push(targetRemote, null, NewB, options); //delete the branch - Repo.Branches.Remove(NewB); - TGServerService.WriteInfo("Pushed reference commit: " + Repo.Head.Tip.Sha, TGServerService.EventID.ReferencePush); - } - catch (Exception e) - { - TGServerService.WriteWarning(String.Format("Failed to push reference commit: {0}. Error: {1}", Repo.Head.Tip.Sha, e.ToString()), TGServerService.EventID.ReferencePush); - } - } - } - - //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 (!LocalIsRemote()) //might be fast forward - PushTestmergeCommit(); - 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 string MergePullRequest(int PRNumber) - { - lock (RepoLock) - { - var result = LoadRepo(); - if (result != null) - return result; - SendMessage(String.Format("REPO: Merging PR #{0}...", PRNumber), 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(); - } - - PushTestmergeCommit(); - } - 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.PathsToStage) - Commands.Stage(Repo, I); - - if (Repo.RetrieveStatus().Staged.Count() == 0) //nothing to commit - return null; - - // Create the committer's signature and commit - var authorandcommitter = MakeSig(); - - // Commit to the repository - TGServerService.WriteInfo(String.Format("Commit {0} created from changelogs", Repo.Commit(CommitMessage, authorandcommitter, authorandcommitter)), TGServerService.EventID.RepoCommit); - DeletePRList(); - return null; - } - catch (Exception e) - { - TGServerService.WriteError("Repo commit failed: " + e.ToString(), TGServerService.EventID.RepoCommitFail); - return e.ToString(); - } - } - } - - //public api - string Push() - { - if (LocalIsRemote()) //nothing to push - return null; - lock (RepoLock) - { - var result = LoadRepo(); - if (result != null) - return result; - try - { - if (!SSHAuth()) - return String.Format("Either {0} or {1} is missing from the server directory. Unable to push!", PrivateKeyPath, PublicKeyPath); - - var options = new PushOptions() - { - CredentialsProvider = GenerateGitCredentials, - }; - Repo.Network.Push(Repo.Network.Remotes[SSHPushRemote], Repo.Head.CanonicalName, options); - TGServerService.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"; - } - - /// - public bool PushTestmergeCommits() - { - return Properties.Settings.Default.PushTestmergeCommits; - } - - /// - public void SetPushTestmergeCommits(bool newValue) - { - Properties.Settings.Default.PushTestmergeCommits = newValue; - } - } -} +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 branch name used for publishing testmerge commits + /// + const string RemoteTempBranchName = "___TGS3TempBranch"; + /// + /// 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(RelativePath(RepoKeyDir)); + if(Exists()) + UpdateBridgeDll(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(RelativePath(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(RelativePath(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(RelativePath(RepoPath))) + return "Repository does not exist"; + try + { + Repo = new Repository(RelativePath(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(RelativePath(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(RelativePath(RepoPath)); + DeletePRList(); + lock (configLock) + { + BackupAndDeleteStaticDirectory(); + } + + var Opts = new CloneOptions() + { + BranchName = BranchName, + RecurseSubmodules = true, + OnTransferProgress = HandleTransferProgress, + OnCheckoutProgress = HandleCheckoutProgress, + CredentialsProvider = GenerateGitCredentials, + }; + + Repository.Clone(RepoURL, RelativePath(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); + WriteInfo("Repository {0}:{1} successfully cloned", EventID.RepoClone); + } + finally + { + currentProgress = -1; + } + } + catch (Exception e) + + { + SendMessage("REPO: Setup failed!", MessageType.DeveloperInfo); + 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() + { + var rsd = RelativePath(StaticDirs); + if (Directory.Exists(rsd)) + { + int count = 1; + + var rsbd = RelativePath(StaticBackupDir); + string path = Path.GetDirectoryName(rsbd); + string newFullPath = rsbd; + + while (File.Exists(newFullPath) || Directory.Exists(newFullPath)) + { + string tempDirName = string.Format("{0}({1})", rsbd, count++); + newFullPath = Path.Combine(path, tempDirName); + } + + Program.CopyDirectory(rsd, newFullPath); + } + Program.DeleteDirectory(rsd); + } + + /// + /// Updates the with the + /// + /// on success, error message on failure + public string UpdateTGS3Json() + { + try + { + if (File.Exists(RelativePath(RepoTGS3SettingsPath))) + File.Copy(RelativePath(RepoTGS3SettingsPath), RelativePath(CachedTGS3SettingsPath), true); + else if (File.Exists(RelativePath(CachedTGS3SettingsPath))) + File.Delete(RelativePath(CachedTGS3SettingsPath)); + } + catch(Exception e) + { + return e.ToString(); + } + return null; + } + + /// + /// Initial setup for the and + /// + void InitialConfigureRepository() + { + Directory.CreateDirectory(RelativePath(StaticDirs)); + UpdateBridgeDll(false); + UpdateTGS3Json(); + var Config = GetCachedRepoConfig(); //RepoBusy is set if we're here + foreach(var I in Config.StaticDirectoryPaths) + { + try + { + var source = Path.Combine(RelativePath(RepoPath), I); + var dest = Path.Combine(RelativePath(StaticDirs), I); + if (Directory.Exists(source)) + Program.CopyDirectory(source, dest); + else + Directory.CreateDirectory(dest); + } + catch + { + WriteError("Could not setup static directory: " + I, EventID.RepoConfigurationFail); + } + } + foreach(var I in Config.DLLPaths) + { + try + { + var source = Path.Combine(RelativePath(RepoPath), I); + if (!File.Exists(source)) + { + WriteWarning("Could not find DLL: " + I, EventID.RepoConfigurationFail); + continue; + } + var dest = Path.Combine(RelativePath(StaticDirs), I); + Program.CopyFileForceDirectories(source, dest, false); + } + catch + { + 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); + WriteInfo("Repo checked out " + sha, EventID.RepoCheckout); + return res; + } + catch (Exception e) + { + SendMessage("REPO: Checkout failed!", MessageType.DeveloperInfo); + 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; + } + + void PushTestmergeCommit() + { + if (Config.PushTestmergeCommits && SSHAuth()) + { + try + { + //now try and push the commit to the remote so they can be referenced + var NewB = Repo.CreateBranch(RemoteTempBranchName).CanonicalName; + + var options = new PushOptions() + { + CredentialsProvider = GenerateGitCredentials + }; + var targetRemote = Repo.Network.Remotes[SSHPushRemote]; + Repo.Network.Push(targetRemote, NewB, options); //push the branch + Repo.Network.Push(targetRemote, null, NewB, options); //delete the branch + Repo.Branches.Remove(NewB); + WriteInfo("Pushed reference commit: " + Repo.Head.Tip.Sha, EventID.ReferencePush); + } + catch (Exception e) + { + WriteWarning(String.Format("Failed to push reference commit: {0}. Error: {1}", Repo.Head.Tip.Sha, e.ToString()), EventID.ReferencePush); + } + } + } + + /// + 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(); + WriteInfo("Repo hard updated to " + originBranch.Tip.Sha, EventID.RepoHardUpdate); + return error; + } + res = MergeBranch(originBranch.FriendlyName); + if (!LocalIsRemote()) //might be fast forward + PushTestmergeCommit(); + if (res != null) + throw new Exception(res); + UpdateSubmodules(); + WriteInfo("Repo merge updated to " + originBranch.Tip.Sha, EventID.RepoMergeUpdate); + return null; + } + catch (Exception E) + { + SendMessage("REPO: Update failed!", MessageType.DeveloperInfo); + 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) + { + try + { + //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); + WriteWarning(msg, EventID.Submodule); + } + catch (Exception ex) + { + WriteError(String.Format("Failed to update submodule {0}! Error: {1}", I.Name, ex.ToString()), EventID.Submodule); + } + } + } + + /// + /// 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) + { + WriteInfo("Repo backup created at tag: " + tagName + " commit: " + HEAD, EventID.RepoBackupTag); + return null; + } + throw new Exception("Tag creation failed!"); + } + } + catch (Exception e) + { + 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(); + WriteInfo(String.Format("Repo branch reset{0}", trackedBranch ? " to tracked branch" : ""), trackedBranch ? EventID.RepoResetTracked : EventID.RepoReset); + return null; + } + 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(RelativePath(PRJobFile))) + try + { + File.Delete(RelativePath(PRJobFile)); + } + catch (Exception e) + { + 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(RelativePath(PRJobFile))) + return new Dictionary>(); + var rawdata = File.ReadAllText(RelativePath(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(RelativePath(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) + { + 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) + { + WriteError("Failed to update PR list", EventID.RepoPRListError); + return "PR Merged, JSON update failed: " + e.ToString(); + } + + PushTestmergeCommit(); + } + return Result; + } + catch (Exception E) + { + SendMessage("REPO: PR merge failed!", MessageType.DeveloperInfo); + 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 + WriteInfo(String.Format("Commit {0} created from changelogs", Repo.Commit(CommitMessage, authorandcommitter, authorandcommitter)), EventID.RepoCommit); + DeletePRList(); + return null; + } + catch (Exception e) + { + 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); + WriteInfo("Repo pushed up to commit: " + Repo.Head.Tip.Sha, EventID.RepoPush); + return null; + } + catch (Exception e) + { + 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(RelativePath(PrivateKeyPath)) && File.Exists(RelativePath(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 = RelativePath(PrivateKeyPath), + PublicKey = RelativePath(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(RelativePath(RepoPath), ChangelogPy))) + { + error = "Missing changelog generation script!"; + return null; + } + + var Config = Properties.Settings.Default; + + var PythonFile = Path.Combine(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(RelativePath(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; + WriteInfo("Changelog generated" + error, EventID.RepoChangelog); + return result; + } + catch (Exception e) + { + error = e.ToString(); + 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 of 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); + } + } + + /// + /// 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"; + } + + /// + public bool PushTestmergeCommits() + { + return Config.PushTestmergeCommits; + } + + /// + public void SetPushTestmergeCommits(bool newValue) + { + Config.PushTestmergeCommits = newValue; + } + } +} diff --git a/TGServerService/ServerInstance/ServerInstance.cs b/TGServerService/ServerInstance/ServerInstance.cs new file mode 100644 index 0000000000..e11f6b8459 --- /dev/null +++ b/TGServerService/ServerInstance/ServerInstance.cs @@ -0,0 +1,185 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.ServiceModel; +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 (<= He's lying through his teeth, don't listen to him) + + //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)] + sealed partial class ServerInstance : IDisposable, ITGConnectivity, ITGInstance + { + /// + /// Used to assign the instance to event IDs + /// + public readonly byte LoggingID; + /// + /// The configuration settings for the instance + /// + readonly InstanceConfig Config; + /// + /// Constructs and a + /// + public ServerInstance(InstanceConfig config, byte logID) + { + LoggingID = logID; + Config = config; + FindTheDroidsWereLookingFor(); + InitEventHandlers(); + InitChat(); + InitRepo(); + InitByond(); + InitCompiler(); + InitDreamDaemon(); + } + + /// + /// Cleans up the + /// + void RunDisposals() + { + DisposeDreamDaemon(); + DisposeCompiler(); + DisposeByond(); + DisposeRepo(); + DisposeChat(); + Config.Save(); + } + + /// + /// Writes information to the Windows event log + /// + /// The log message + /// The of the message + void WriteInfo(string message, EventID id) + { + Service.WriteEntry(message, id, EventLogEntryType.Information, LoggingID); + } + + /// + /// Writes an error to the Windows event log + /// + /// The log message + /// The of the message + void WriteError(string message, EventID id) + { + Service.WriteEntry(message, id, EventLogEntryType.Error, LoggingID); + } + + /// + /// Writes a warning to the Windows event log + /// + /// The log message + /// The of the message + void WriteWarning(string message, EventID id) + { + Service.WriteEntry(message, id, EventLogEntryType.Warning, LoggingID); + } + + /// + /// Writes an access event to the Windows event log + /// + /// The (un)authenticated Windows user's name + /// if authenticated sucessfully, otherwise + void WriteAccess(string username, bool authSuccess) + { + Service.WriteEntry(String.Format("Access from: {0}", username), EventID.Authentication, authSuccess ? EventLogEntryType.SuccessAudit : EventLogEntryType.FailureAudit, LoggingID); + } + + /// + /// Converts relative paths to full directory paths + /// + /// + string RelativePath(string path) + { + return Path.Combine(Config.Directory, path); + } + + /// + public string Version() + { + return Service.VersionString; + } + + /// + public void VerifyConnection() { } + + /// + public void Reattach(bool silent) + { + Config.ReattachRequired = true; + if(!silent) + SendMessage("SERVICE: Update started...", MessageType.DeveloperInfo); + } + + /// + public string ServerDirectory() + { + return Config.Directory; + } + + /// + /// Sets of to + /// + public void Offline() + { + Config.Enabled = false; + } + + //mostly generated code with a call to RunDisposals() + //you don't need to open this + #region IDisposable Support + /// + /// To detect redundant calls + /// + private bool disposedValue = false; + + /// + /// Implements the pattern. Calls + /// + /// if was called manually, if it was from the finalizer + void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + RunDisposals(); + // TODO: dispose managed state (managed objects). + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + disposedValue = true; + } + } + + // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. + // ~TGStationServer() { + // // 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. + Dispose(true); + // TODO: uncomment the following line if the finalizer is overridden above. + // GC.SuppressFinalize(this); + } + #endregion + } +} diff --git a/TGServerService/ServerService.Designer.cs b/TGServerService/ServerService.Designer.cs deleted file mode 100644 index 029f1291c9..0000000000 --- a/TGServerService/ServerService.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/ServerService.cs b/TGServerService/ServerService.cs deleted file mode 100644 index c2adb435bc..0000000000 --- a/TGServerService/ServerService.cs +++ /dev/null @@ -1,240 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Security.Principal; -using System.ServiceModel; -using System.ServiceProcess; -using TGServiceInterface; - -namespace TGServerService -{ - public partial class TGServerService : ServiceBase - { - //only deprecate events, do not reuse them - public enum EventID - { - ChatCommand = 100, - ChatConnectFail = 200, - ChatProviderStartFail = 300, - InvalidChatProvider = 400, - UpdateRequest = 500, - BYONDUpdateFail = 600, - BYONDUpdateStaged = 700, - BYONDUpdateComplete = 800, - ServerMoveFailed = 900, - ServerMovePartial = 1000, - ServerMoveComplete = 1100, - DMCompileCrash = 1200, - DMInitializeCrash = 1300, - DMCompileError = 1400, - DMCompileSuccess = 1500, - DMCompileCancel = 1600, - DDReattachFail = 1700, - DDReattachSuccess = 1800, - DDWatchdogCrash = 1900, - DDWatchdogExit = 2000, - DDWatchdogRebootedServer = 2100, - DDWatchdogRebootingServer = 2200, - DDWatchdogRestart = 2300, - DDWatchdogRestarted = 2400, - DDWatchdogStarted = 2500, - ChatSend = 2600, - ChatBroadcast = 2700, - //ChatAdminBroadcast = 2800, - ChatDisconnectFail = 2900, - //TopicSent = 3000, - //TopicFailed = 3100, - CommsKeySet = 3200, - NudgeStartFail = 3300, - NudgeCrash = 3400, - RepoClone = 3500, - RepoCloneFail = 3600, - RepoCheckout = 3700, - RepoCheckoutFail = 3800, - RepoHardUpdate = 3900, - RepoHardUpdateFail = 4000, - RepoMergeUpdate = 4100, - RepoMergeUpdateFail = 4200, - RepoBackupTag = 4300, - RepoBackupTagFail = 4400, - RepoResetTracked = 4500, - RepoResetTrackedFail = 4600, - RepoReset = 4700, - RepoResetFail = 4800, - RepoPRListError = 4900, - RepoPRMerge = 5000, - RepoPRMergeFail = 5100, - RepoCommit = 5200, - RepoCommitFail = 5300, - RepoPush = 5400, - RepoPushFail = 5500, - RepoChangelog = 5600, - RepoChangelogFail = 5700, - ServiceShutdownFail = 6100, - WorldReboot = 6200, - ServerUpdateApplied = 6300, - ChatBroadcastFail = 6400, - IRCLogModes = 6500, - SubmoduleReclone = 6600, - Authentication = 6700, - PreactionEvent = 6800, - PreactionFail = 6900, - InteropCallException = 7000, - APIVersionMismatch = 7100, - RepoConfigurationFail = 7200, - StaticRead = 7300, - StaticWrite = 7400, - StaticDelete = 7500, - ReferencePush = 7600, - } - - static TGServerService ActiveService; //So everyone else can write to our eventlog - - public static readonly string Version = "/tg/station 13 Server Service v" + FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).FileVersion; - - /// - /// You can't write to logs while impersonating, call this to cancel WCF's impersonation first - /// - public static void CancelImpersonation() - { - WindowsIdentity.Impersonate(IntPtr.Zero); - } - - public static void WriteInfo(string message, EventID id) - { - ActiveService.EventLog.WriteEntry(message, EventLogEntryType.Information, (int)id); - } - public static void WriteError(string message, EventID id) - { - ActiveService.EventLog.WriteEntry(message, EventLogEntryType.Error, (int)id); - } - public static void WriteWarning(string message, EventID id) - { - ActiveService.EventLog.WriteEntry(message, EventLogEntryType.Warning, (int)id); - } - - 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); - } - - ServiceHost host; //the WCF host - - void MigrateSettings(int oldVersion, int newVersion) - { - if (oldVersion == newVersion && newVersion == 0) //chat refactor - Properties.Settings.Default.ChatProviderData = "NEEDS INITIALIZING"; //reset chat settings to be safe - } - - //you should seriously not add anything here - //Use OnStart instead - public TGServerService() - { - try - { - if (Properties.Settings.Default.UpgradeRequired) - { - var newVersion = Properties.Settings.Default.SettingsVersion; - Properties.Settings.Default.Upgrade(); - var oldVersion = Properties.Settings.Default.SettingsVersion; - Properties.Settings.Default.SettingsVersion = newVersion; - - MigrateSettings(oldVersion, newVersion); - - Properties.Settings.Default.UpgradeRequired = false; - Properties.Settings.Default.Save(); - } - InitializeComponent(); - ActiveService = this; - Run(this); - } - finally - { - Properties.Settings.Default.Save(); - } - } - - //when babby is formed - protected override void OnStart(string[] args) - { - var Config = Properties.Settings.Default; - if (!Directory.Exists(Config.ServerDirectory)) - { - EventLog.WriteEntry("Creating server directory: " + Config.ServerDirectory); - Directory.CreateDirectory(Config.ServerDirectory); - } - Environment.CurrentDirectory = Config.ServerDirectory; - - var instance = new TGStationServer(); - - for (var I = 0; I < args.Length - 1; ++I) - if (args[I].ToLower() == "-port") - { - try - { - var res = Convert.ToUInt16(args[I + 1]); - if (res == 0) - throw new Exception("Cannot bind to port 0"); - Config.RemoteAccessPort = res; - } - catch(Exception e) - { - throw new Exception("Invalid argument for \"-port\"", e); - } - Config.Save(); - break; - } - - host = new ServiceHost(instance, new Uri[] { new Uri("net.pipe://localhost"), new Uri(String.Format("https://localhost:{0}", Config.RemoteAccessPort)) }) - { - CloseTimeout = new TimeSpan(0, 0, 5) - }; - - foreach (var I in Server.ValidInterfaces) - AddEndpoint(I); - - host.Authorization.ServiceAuthorizationManager = instance; - - try - { - host.Open(); - } - catch (AddressAlreadyInUseException e) - { - throw new Exception("Can't start the service due to the configured remote access port being in use. To fix this change it by starting the service with the \"-port \" argument.", e); - } - } - - //shorthand for adding the WCF endpoint - void AddEndpoint(Type typetype) - { - var bindingName = Server.MasterInterfaceName + "/" + typetype.Name; - host.AddServiceEndpoint(typetype, new NetNamedPipeBinding() { SendTimeout = new TimeSpan(0, 0, 30), MaxReceivedMessageSize = Server.TransferLimitLocal }, bindingName); - var httpsBinding = new WSHttpBinding() - { - SendTimeout = new TimeSpan(0, 0, 40), - MaxReceivedMessageSize = Server.TransferLimitRemote - }; - var requireAuth = typetype.Name != typeof(ITGConnectivity).Name; - httpsBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None; - httpsBinding.Security.Mode = requireAuth ? SecurityMode.TransportWithMessageCredential : SecurityMode.Transport; //do not require auth for a connectivity check - httpsBinding.Security.Message.ClientCredentialType = requireAuth ? MessageCredentialType.UserName : MessageCredentialType.None; - host.AddServiceEndpoint(typetype, httpsBinding, bindingName); - } - - //when we is kill - protected override void OnStop() - { - try - { - TGStationServer instance = (TGStationServer)host.SingletonInstance; - host.Close(); - instance.Dispose(); - } - catch (Exception e) - { - WriteError(e.ToString(), EventID.ServiceShutdownFail); - } - } - } -} diff --git a/TGServerService/ServerService.resx b/TGServerService/ServerService.resx deleted file mode 100644 index e5858cc294..0000000000 --- a/TGServerService/ServerService.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/Service.cs b/TGServerService/Service.cs new file mode 100644 index 0000000000..b9d8547453 --- /dev/null +++ b/TGServerService/Service.cs @@ -0,0 +1,698 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; +using System.Security.Principal; +using System.ServiceModel; +using System.ServiceProcess; +using TGServiceInterface; +using TGServiceInterface.Components; + +namespace TGServerService +{ + /// + /// The windows service the application runs as + /// + [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.Single)] + class Service : ServiceBase, ITGSService, ITGConnectivity + { + /// + /// The logging ID used for events + /// + public const byte LoggingID = 0; + + /// + /// 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; + + /// + /// 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 an event to Windows the event log + /// + /// The log message + /// The of the message + /// The of the event + /// The logging source ID for the event + public static void WriteEntry(string message, EventID id, EventLogEntryType eventType, byte loggingID) + { + ActiveService.EventLog.WriteEntry(message, eventType, (int)id + loggingID); + } + + /// + /// Checks an for illegal characters + /// + /// The name to check + /// if contains no illegal characters, error message otherwise + static string CheckInstanceName(string instanceName) + { + char[] bannedCharacters = { ';', '&', '=', '%' }; + foreach (var I in bannedCharacters) + if (instanceName.Contains(I.ToString())) + return "Instance names may not contain the following characters: ';', '&', '=', or '%'"; + return null; + } + + /// + /// Sets up ServiceName and + /// + public Service() + { + ServiceName = "TG Station Server"; + if (ActiveService != null) + throw new Exception("There is already a Service instance running!"); + ActiveService = this; + } + + /// + /// Clears + /// + /// if this method was invoked from , otherwise it was invoked by the finalizer + protected override void Dispose(bool disposing) + { + ActiveService = null; + base.Dispose(disposing); + } + + /// + /// The WCF host that contains connects to + /// + ServiceHost serviceHost; + /// + /// Map of to the respective hosting the + /// + IDictionary hosts; + /// + /// List of s in use + /// + IList UsedLoggingIDs = new List(); + + /// + /// Migrates the .NET config from to + 1 + /// + /// The version to migrate from + void MigrateSettings(int oldVersion) + { + var Config = Properties.Settings.Default; + switch (oldVersion) + { + case 6: //switch to per-instance configs + var IC = DeprecatedInstanceConfig.CreateFromNETSettings(); + IC.Save(); + Config.InstancePaths.Add(IC.Directory); + break; + } + } + + /// + /// Enumerates configured s. Detaches those that fail to load + /// + /// Each configured + IEnumerable GetInstanceConfigs() + { + var pathsToRemove = new List(); + lock (this) + { + var IPS = Properties.Settings.Default.InstancePaths; + foreach (var I in IPS) + { + InstanceConfig ic; + try + { + ic = InstanceConfig.Load(I); + } + catch (Exception e) + { + WriteEntry(String.Format("Unable load instance config at path {0}. Error: {1} Detaching...", I, e.ToString()), EventID.InstanceInitializationFailure, EventLogEntryType.Error, LoggingID); + pathsToRemove.Add(I); + continue; + } + yield return ic; + } + foreach (var I in pathsToRemove) + IPS.Remove(I); + } + } + + /// + /// Overrides and saves the configured if requested by command line parameters + /// + /// The command line parameters for the + void ChangePortFromCommandLine(string[] args) + { + var Config = Properties.Settings.Default; + + for (var I = 0; I < args.Length - 1; ++I) + if (args[I].ToLower() == "-port") + { + try + { + var res = Convert.ToUInt16(args[I + 1]); + if (res == 0) + throw new Exception("Cannot bind to port 0"); + Config.RemoteAccessPort = res; + } + catch (Exception e) + { + throw new Exception("Invalid argument for \"-port\"", e); + } + Config.Save(); + break; + } + } + + /// + /// Called by the Windows service manager. Initializes and starts configured s + /// + /// Command line arguments for the + protected override void OnStart(string[] args) + { + Environment.CurrentDirectory = Directory.CreateDirectory(Path.GetTempPath() + "/TGStationServerService").FullName; //MOVE THIS POINTER BECAUSE ONE TIME I ALMOST ACCIDENTALLY NUKED MYSELF BY REFACTORING! http://imgur.com/zvGEpJD.png + + SetupConfig(); + + ChangePortFromCommandLine(args); + + SetupService(); + + SetupInstances(); + + OnlineAllHosts(); + } + + /// + /// Writes some changes to the that always need to be done. + /// + void PrePrepConfig() + { + var Config = Properties.Settings.Default; + + if (Config.InstancePaths == null) + Config.InstancePaths = new StringCollection(); + } + + /// + /// Upgrades up the service configuration + /// + void SetupConfig() + { + var Config = Properties.Settings.Default; + if (Config.UpgradeRequired) + { + var newVersion = Config.SettingsVersion; + Config.Upgrade(); + + PrePrepConfig(); + + for (var oldVersion = Config.SettingsVersion; oldVersion < newVersion; ++oldVersion) + MigrateSettings(oldVersion); + + Config.SettingsVersion = newVersion; + + Config.UpgradeRequired = false; + Config.Save(); + } + } + + /// + /// Creates the for + /// + void SetupService() + { + serviceHost = CreateHost(this, Interface.MasterInterfaceName); + AddEndpoint(serviceHost, typeof(ITGSService)); + AddEndpoint(serviceHost, typeof(ITGConnectivity)); + serviceHost.Authorization.ServiceAuthorizationManager = new AdministrativeAuthorizationManager(); //only admins can diddle us + } + + /// + /// Opens all created s + /// + void OnlineAllHosts() + { + serviceHost.Open(); + foreach (var I in hosts) + I.Value.Open(); + } + + /// + /// Creates a for using the default pipe, CloseTimeout for the , and the configured + /// + /// The + /// The URL to access components on the + /// The created + static ServiceHost CreateHost(object singleton, string endpointPostfix) + { + return new ServiceHost(singleton, new Uri[] { new Uri(String.Format("net.pipe://localhost/{0}", endpointPostfix)), new Uri(String.Format("https://localhost:{0}/{1}", Properties.Settings.Default.RemoteAccessPort, endpointPostfix)) }) + { + CloseTimeout = new TimeSpan(0, 0, 5) + }; + } + + /// + /// Creates s for all s as listed in , detaches bad ones + /// + void SetupInstances() + { + hosts = new Dictionary(); + var pathsToRemove = new List(); + var seenNames = new List(); + foreach (var I in GetInstanceConfigs()) + { + if (seenNames.Contains(I.Name)) + { + WriteEntry(String.Format("Instance at {0} has a duplicate name! Detaching...", I.Directory), EventID.InstanceInitializationFailure, EventLogEntryType.Error, LoggingID); + pathsToRemove.Add(I.Directory); + } + if (SetupInstance(I) == null) + pathsToRemove.Add(I.Directory); + else + seenNames.Add(I.Name); + } + foreach (var I in pathsToRemove) + Properties.Settings.Default.InstancePaths.Remove(I); + } + + /// + /// Unlocks a acquired with + /// + /// The to unlock + void UnlockLoggingID(byte ID) + { + lock (UsedLoggingIDs) + { + UsedLoggingIDs.Remove(ID); + } + } + + /// + /// Gets and locks a + /// + /// A logging ID for the must be released using + byte LockLoggingID() + { + lock (UsedLoggingIDs) + { + for (byte I = 1; I < 100; ++I) + if (!UsedLoggingIDs.Contains(I)) + { + UsedLoggingIDs.Add(I); + return I; + } + } + throw new Exception("All logging IDs in use!"); + } + + /// + /// Creates and starts a for a at + /// + /// The for the + /// The inactive on success, on failure + ServiceHost SetupInstance(InstanceConfig config) + { + ServerInstance instance; + string instanceName; + try + { + if (hosts.ContainsKey(config.Directory)) + { + var datInstance = ((ServerInstance)hosts[config.Directory].SingletonInstance); + WriteEntry(String.Format("Unable to start instance at path {0}. Has the same name as instance at path {1}. Detaching...", config.Directory, datInstance.ServerDirectory()), EventID.InstanceInitializationFailure, EventLogEntryType.Error, LoggingID); + Properties.Settings.Default.InstancePaths.Remove(config.Directory); + return null; + } + if (!config.Enabled) + return null; + var ID = LockLoggingID(); + WriteEntry(String.Format("Instance {0} ({1}) assigned logging ID {2}", config.Name, config.Directory, ID), EventID.InstanceIDAssigned, EventLogEntryType.Information, ID); + instanceName = config.Name; + instance = new ServerInstance(config, ID); + } + catch (Exception e) + { + WriteEntry(String.Format("Unable to start instance at path {0}. Detaching... Error: {1}", config.Directory, e.ToString()), EventID.InstanceInitializationFailure, EventLogEntryType.Error, LoggingID); + return null; + } + + var host = CreateHost(instance, String.Format("{0}/{1}", Interface.InstanceInterfaceName, instanceName)); + hosts.Add(instanceName, host); + + foreach (var J in Interface.ValidInterfaces) + AddEndpoint(host, J); + + host.Authorization.ServiceAuthorizationManager = instance; + return host; + } + + /// + /// Adds a WCF endpoint for a component + /// + /// The service host to add the component to + /// The type of the component + void AddEndpoint(ServiceHost host, Type typetype) + { + var bindingName = typetype.Name; + 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 = Interface.TransferLimitRemote + }; + var requireAuth = typetype.Name != typeof(ITGConnectivity).Name; + httpsBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None; + httpsBinding.Security.Mode = requireAuth ? SecurityMode.TransportWithMessageCredential : SecurityMode.Transport; //do not require auth for a connectivity check + httpsBinding.Security.Message.ClientCredentialType = requireAuth ? MessageCredentialType.UserName : MessageCredentialType.None; + host.AddServiceEndpoint(typetype, httpsBinding, bindingName); + } + + /// + /// Shuts down all active s and calls on it's + /// + protected override void OnStop() + { + lock (this) + { + try + { + foreach (var I in hosts) + { + var host = I.Value; + var instance = (ServerInstance)host.SingletonInstance; + host.Close(); + instance.Dispose(); + UnlockLoggingID(instance.LoggingID); + } + } + catch (Exception e) + { + WriteEntry(e.ToString(), EventID.ServiceShutdownFail, EventLogEntryType.Error, LoggingID); + } + serviceHost.Close(); + } + Properties.Settings.Default.Save(); + ActiveService = null; + } + + /// + public void VerifyConnection() { } + + /// + public void PrepareForUpdate() + { + foreach (var I in hosts) + ((ServerInstance)I.Value.SingletonInstance).Reattach(false); + } + + /// + public ushort RemoteAccessPort() + { + return Properties.Settings.Default.RemoteAccessPort; + } + + /// + public string SetRemoteAccessPort(ushort port) + { + if (port == 0) + return "Cannot bind to port 0"; + Properties.Settings.Default.RemoteAccessPort = port; + return null; + } + + /// + public string Version() + { + return VersionString; + } + + /// + 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; + } + + /// + public IList ListInstances() + { + var result = new List(); + lock (this) + foreach (var ic in GetInstanceConfigs()) + result.Add(new InstanceMetadata + { + Name = ic.Name, + Path = ic.Directory, + Enabled = ic.Enabled, + LoggingID = (byte)(ic.Enabled ? ((ServerInstance)hosts[ic.Name].SingletonInstance).LoggingID : 0) + }); + return result; + } + + /// + public string CreateInstance(string Name, string path) + { + var res = CheckInstanceName(Name); + if (res != null) + return res; + if (File.Exists(path) || Directory.Exists(path)) + return "Cannot create instance at pre-existing path!"; + var Config = Properties.Settings.Default; + lock (this) + { + if (Config.InstancePaths.Contains(path)) + return String.Format("Instance at {0} already exists!", path); + foreach (var oic in GetInstanceConfigs()) + if (Name == oic.Name) + return String.Format("Instance named {0} already exists!", oic.Name); + InstanceConfig ic; + try + { + ic = new InstanceConfig(path) + { + Name = Name + }; + Directory.CreateDirectory(path); + ic.Save(); + Properties.Settings.Default.InstancePaths.Add(path); + } + catch (Exception e) + { + return e.ToString(); + } + return SetupOneInstance(ic); + } + } + + /// + /// Starts and onlines an instance located at + /// + /// The for the + /// on success, error message on failure + string SetupOneInstance(InstanceConfig config) + { + try + { + var host = SetupInstance(config); + if (host != null) + host.Open(); + else + lock (this) + Properties.Settings.Default.InstancePaths.Remove(config.Directory); + return null; + } + catch (Exception e) + { + return "Instance set up but an error occurred while starting it: " + e.ToString(); + } + } + + /// + public string ImportInstance(string path) + { + var Config = Properties.Settings.Default; + lock (this) + { + if (Config.InstancePaths.Contains(path)) + return String.Format("Instance at {0} already exists!", path); + if(!Directory.Exists(path)) + return String.Format("There is no instance located at {0}!", path); + InstanceConfig ic; + try + { + ic = InstanceConfig.Load(path); + foreach(var oic in GetInstanceConfigs()) + if(ic.Name == oic.Name) + return String.Format("Instance named {0} already exists!", oic.Name); + ic.Save(); + Properties.Settings.Default.InstancePaths.Add(path); + } + catch (Exception e) + { + return e.ToString(); + } + return SetupOneInstance(ic); + } + } + + /// + public bool InstanceEnabled(string Name) + { + lock(this) + { + return hosts.ContainsKey(Name); + } + } + + /// + public string SetInstanceEnabled(string Name, bool enabled) + { + return SetInstanceEnabledImpl(Name, enabled, out string path); + } + + + /// + /// Sets a 's enabled status + /// + /// The whom's status should be changed + /// to enable the , to disable it + /// The path to the modified + /// on success, error message on failure + string SetInstanceEnabledImpl(string Name, bool enabled, out string path) + { + path = null; + lock (this) + { + var hostIsOnline = hosts.ContainsKey(Name); + if (enabled) + { + if (hostIsOnline) + return null; + //now this is a bit awkward because we need to check each instance config for the one named Name + string LastCheckedConfig = null; + try + { + foreach (var ic in GetInstanceConfigs()) + { + if (ic.Name == Name) + { + path = ic.Directory; + ic.Enabled = true; + return SetupOneInstance(ic); + } + } + } + catch (Exception e) + { + return String.Format("An error occurred while checking instance config at {0}! Error: ", LastCheckedConfig, e.ToString()); + } + return String.Format("Instance {0} does not exist!", Name); + } + else + { + if (!hostIsOnline) + return null; + var host = hosts[Name]; + hosts.Remove(Name); + var inst = (ServerInstance)host.SingletonInstance; + host.Close(); + path = inst.ServerDirectory(); + inst.Offline(); + inst.Dispose(); + UnlockLoggingID(inst.LoggingID); + return null; + } + } + } + + /// + public string RenameInstance(string name, string new_name) + { + if (name == new_name) + return null; + var res = CheckInstanceName(new_name); + if (res != null) + return res; + lock (this) + { + //we have to check em all anyway + InstanceConfig the_droid_were_looking_for = null; + foreach (var ic in GetInstanceConfigs()) + if (ic.Name == name) + { + the_droid_were_looking_for = ic; + break; + } + else if (ic.Name == new_name) + return String.Format("There is already another instance named {0}!", new_name); + if (the_droid_were_looking_for == null) + return String.Format("There is no instance named {0}!", name); + var ie = InstanceEnabled(name); + if(ie) + SetInstanceEnabled(name, false); + the_droid_were_looking_for.Name = new_name; + string result = ""; + try + { + the_droid_were_looking_for.Save(); + result = null; + } + catch(Exception e) + { + result = "Could not save instance config! Error: " + e.ToString(); + } + finally + { + if (ie) + { + var resRestore = SetInstanceEnabled(new_name, true); + if (resRestore != null) + result = (result + " " + resRestore).Trim(); + } + } + return result; + } + } + + /// + public string DetachInstance(string name) + { + lock (this) + { + var res = SetInstanceEnabledImpl(name, false, out string path); + if (res != null) + return res; + if (path == null) //gotta find it ourselves + foreach (var ic in GetInstanceConfigs()) + if (ic.Name == name) + { + path = ic.Directory; + break; + } + if (path == null) + return String.Format("No instance named {0} exists!", name); + Properties.Settings.Default.InstancePaths.Remove(path); + return null; + } + } + } +} diff --git a/TGServerService/TGServerService.csproj b/TGServerService/TGServerService.csproj index c4c6aa7583..375c542010 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\Debug\ + DEBUG;TRACE + full + AnyCPU + prompt + MinimumRecommendedRules.ruleset + true + + + bin\Release\ + TRACE + bin\x86\Release\TGServerService.xml + true + true + pdbonly + AnyCPU + prompt + MinimumRecommendedRules.ruleset + true + ..\packages\Discord.Net.Core.1.0.2\lib\net45\Discord.Net.Core.dll @@ -63,6 +65,7 @@ ..\packages\System.Collections.Immutable.1.3.1\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + @@ -80,37 +83,46 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + True True Settings.settings - + Component - - ProjectInstaller.cs - - + Component - - ServerService.cs - @@ -127,14 +139,6 @@ Settings.Designer.cs - - - ProjectInstaller.cs - - - ServerService.cs - - {ac4e7e8b-f83a-481c-a8b0-8fa4e8ae59ab} @@ -142,9 +146,9 @@ - + diff --git a/TGServiceInstaller/Product.wxs b/TGServiceInstaller/Product.wxs index ed5c40da2e..0a917fac53 100644 --- a/TGServiceInstaller/Product.wxs +++ b/TGServiceInstaller/Product.wxs @@ -47,11 +47,11 @@ INSTALLSHORTCUTSTART = 1 INSTALLSHORTCUTDESK = 1 @@ -119,6 +119,9 @@ + + + diff --git a/TGServiceInstaller/TGServiceInstaller.wixproj b/TGServiceInstaller/TGServiceInstaller.wixproj index d57b13bb7d..dbe628e0fd 100644 --- a/TGServiceInstaller/TGServiceInstaller.wixproj +++ b/TGServiceInstaller/TGServiceInstaller.wixproj @@ -18,6 +18,7 @@ bin\$(Configuration)\ obj\$(Configuration)\ True + True @@ -39,6 +40,14 @@ Binaries;Content;Satellites INSTALLFOLDER + + TGDreamDaemonBridge + {9a01ef03-8eae-45cb-8b87-4a17bd904557} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + TGServerService {f32eda25-0855-411c-af5e-f0d042917e2d} diff --git a/TGServiceInterface/Administration.cs b/TGServiceInterface/Administration.cs deleted file mode 100644 index fbb7964003..0000000000 --- a/TGServiceInterface/Administration.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.ServiceModel; - -namespace TGServiceInterface -{ - /// - /// Manage the group that is used to access the service, can only be used by an administrator - /// - [ServiceContract] - public interface ITGAdministration - { - /// - /// Get the port used for remote operation - /// - /// The port used for remote operation - [OperationContract] - ushort RemoteAccessPort(); - - /// - /// Set the port used for remote operation - /// Requires a service restart to take effect - /// - /// The new port to use for remote operation - /// null on success, error message on failure - [OperationContract] - string SetRemoteAccessPort(ushort port); - - /// - /// 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 - [OperationContract] - string GetCurrentAuthorizedGroup(); - - /// - /// 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 - [OperationContract] - string SetAuthorizedGroup(string groupName); - - /// - /// Moves the entire server installation, requires no operations to be running - /// - /// The new path to place the server - /// null on success, error message on failure - [OperationContract] - string MoveServer(string new_location); - - /// - /// 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 - [OperationContract] - string RecreateStaticFolder(); - } -} 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 66% rename from TGServiceInterface/Chat.cs rename to TGServiceInterface/ChatSetupInfo.cs index cfdab6d228..c34d6aea8e 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,28 @@ 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 + /// + /// The that this is for /// Optional past data /// The number of fields in this chat provider - protected TGChatSetupInfo(TGChatSetupInfo baseInfo, int numFields) + protected internal ChatSetupInfo(ChatProvider provider, ChatSetupInfo baseInfo, int numFields) { numFields += BaseIndex; InitializeFields = baseInfo == null || baseInfo.DataFields.Count != numFields; @@ -88,47 +63,68 @@ namespace TGServiceInterface } else DataFields = baseInfo.DataFields; - } - - TGChatSetupInfo Specialize() - { - switch (Provider) - { - case TGChatProvider.IRC: - return new TGIRCSetupInfo(this); - case TGChatProvider.Discord: - return new TGDiscordSetupInfo(this); - default: - throw new Exception("Invalid provider!"); - } - } - - //trims and adds the leading # - protected virtual string SanitizeChannelName(string working) - { - 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()); + Provider = provider; + Specialize(true); //to check we have a valid provider } /// - /// Constructs a TGChatSetupInfo from a data list + /// Recreates as the correct child + /// + /// If , is returned provided is a valid + /// A new based on the type + ChatSetupInfo Specialize(bool checkOnly) + { + switch (Provider) + { + case ChatProvider.IRC: + if (!checkOnly) + return new IRCSetupInfo(this); + break; + case ChatProvider.Discord: + if (!checkOnly) + return new DiscordSetupInfo(this); + break; + default: + throw new Exception("Invalid provider!"); + } + return null; + } + + /// + /// Properly formats a name for the + /// + /// The to format + /// The formatted + protected virtual string SanitizeChannelName(string channel) + { + return Specialize(false).SanitizeChannelName(channel); + } + + /// + /// 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; + Specialize(false); //ensure provider type is valid } /// /// The list of admin entries @@ -206,25 +202,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; @@ -232,27 +221,27 @@ namespace TGServiceInterface const int AuthTargetIndex = 3; const int AuthMessageIndex = 4; const int AuthLevelIndex = 5; - const int FieldsLen = 6; - + const int FieldsLen = 6; + /// /// 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(ChatProvider.IRC, baseInfo, FieldsLen) { - Provider = TGChatProvider.IRC; - if (InitializeFields) - { - Nickname = "TGS3"; - URL = "irc.rizon.net"; - Port = 6667; - AuthTarget = ""; - AuthMessage = ""; - AdminsAreSpecial = true; - AuthLevel = IRCMode.Op; - } + if (!InitializeFields) + return; + + Nickname = "TGS3"; + URL = "irc.rizon.net"; + Port = 6667; + AuthTarget = ""; + AuthMessage = ""; + AdminsAreSpecial = true; + AuthLevel = IRCMode.Op; } - + + /// protected override string SanitizeChannelName(string working) { if (working[0] != '#') @@ -263,9 +252,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,27 +300,27 @@ 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; + const int FieldsLen = 1; /// /// 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(ChatProvider.Discord, baseInfo, FieldsLen) { - Provider = TGChatProvider.Discord; - if (InitializeFields) - BotToken = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; //needless to say, this is fake + if (!InitializeFields) + return; + 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 +341,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..5355a0d55e 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 : int + { + /// + /// 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/Components/Administration.cs b/TGServiceInterface/Components/Administration.cs new file mode 100644 index 0000000000..b177312820 --- /dev/null +++ b/TGServiceInterface/Components/Administration.cs @@ -0,0 +1,33 @@ +using System.ServiceModel; + +namespace TGServiceInterface.Components +{ + /// + /// Manage the group that is used to access the service, can only be used by an administrator + /// + [ServiceContract] + public interface ITGAdministration + { + /// + /// 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, on failure + [OperationContract] + string GetCurrentAuthorizedGroup(); + + /// + /// 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, 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 + /// + /// 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 91% rename from TGServiceInterface/Config.cs rename to TGServiceInterface/Components/Config.cs index 55027423a4..1055352aad 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 @@ -11,12 +10,6 @@ namespace TGServiceInterface [ServiceContract] public interface ITGConfig { - /// - /// Return the directory of the server on the host machine - /// - /// The path to the directory on success, null on failure - [OperationContract] - string ServerDirectory(); /// /// Returns the file contents of the specified server directory diff --git a/TGServiceInterface/Connectivity.cs b/TGServiceInterface/Components/Connectivity.cs similarity index 68% rename from TGServiceInterface/Connectivity.cs rename to TGServiceInterface/Components/Connectivity.cs index 5957be6bd3..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 @@ -13,5 +13,12 @@ namespace TGServiceInterface /// [OperationContract] void VerifyConnection(); + + /// + /// Retrieve's the service's version + /// + /// The service's version + [OperationContract] + string Version(); } } 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/Instance.cs b/TGServiceInterface/Components/Instance.cs new file mode 100644 index 0000000000..9172aee46b --- /dev/null +++ b/TGServiceInterface/Components/Instance.cs @@ -0,0 +1,18 @@ +using System.ServiceModel; + +namespace TGServiceInterface.Components +{ + /// + /// Metadata for a server instance + /// + [ServiceContract] + public interface ITGInstance + { + /// + /// Return the directory of the server on the host machine + /// + /// The path to the directory on success, null on failure + [OperationContract] + string ServerDirectory(); + } +} diff --git a/TGServiceInterface/Components/Interop.cs b/TGServiceInterface/Components/Interop.cs new file mode 100644 index 0000000000..e57e74cb08 --- /dev/null +++ b/TGServiceInterface/Components/Interop.cs @@ -0,0 +1,19 @@ +using System.ServiceModel; + +namespace TGServiceInterface.Components +{ + /// + /// Used by DreamDaemon to access the interop API with call()(). Restrictions are in place so that only a DreamDaemon instance launched by the service can use this API + /// + [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 52% rename from TGServiceInterface/Repository.cs rename to TGServiceInterface/Components/Repository.cs index 2821e4bae5..5a64167aad 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,60 +135,44 @@ 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(); - /// - /// Sets the path to the python 2.7 installation + /// Pushes the paths listed in TGS3.json to the currentl git remote. No other commit differences may exist for this function to succeed /// - /// The new path - /// true if the path exists, false otherwise + /// on success, error on failure [OperationContract] - bool SetPythonPath(string path); - - /// - /// Gets the path to the python 2.7 installation - /// - /// The path to the python 2.7 installation - [OperationContract] - string PythonPath(); + string SynchronizePush(); /// /// 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/Components/Service.cs b/TGServiceInterface/Components/Service.cs new file mode 100644 index 0000000000..e9057d957c --- /dev/null +++ b/TGServiceInterface/Components/Service.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using System.ServiceModel; + +namespace TGServiceInterface.Components +{ + /// + /// Interface for managing the service + /// + [ServiceContract] + public interface ITGSService + { + + /// + /// Next stop of the service will not close DD and sets a flag for it to reattach once it restarts + /// + [OperationContract] + void PrepareForUpdate(); + + /// + /// Retrieve's the service's version + /// + /// The service's version + [OperationContract] + string Version(); + + /// + /// Get the port used for remote operation + /// + /// The port used for remote operation + [OperationContract] + ushort RemoteAccessPort(); + + /// + /// Set the port used for remote operation + /// Requires a service restart to take effect + /// + /// The new port to use for remote operation + /// null on success, error message on failure + [OperationContract] + string SetRemoteAccessPort(ushort port); + + /// + /// List instances + /// + /// A of instance names relating to their paths + [OperationContract] + IList ListInstances(); + + /// + /// Creates a new server instance + /// + /// The name of the instance + /// The path to the instance + /// on success, error message on failure + [OperationContract] + string CreateInstance(string Name, string path); + + /// + /// Registers an existing server instance + /// + /// The path to the instance + /// on success, error message on failure + [OperationContract] + string ImportInstance(string path); + + /// + /// Checks if an instance is online + /// + /// The name of the instance + /// if the Instance exists and is online, otherwise + [OperationContract] + bool InstanceEnabled(string Name); + + /// + /// Sets an instance's enabled status + /// + /// The instance whom's status should be changed + /// to enable the instance, to disable it + /// on success, error message on failure + [OperationContract] + string SetInstanceEnabled(string Name, bool enabled); + + /// + /// Renames an instance, this will restart the instance if it is enabled + /// + /// The current name of the instance + /// The new name of the instance + /// on success, error message on failure + [OperationContract] + string RenameInstance(string name, string new_name); + + /// + /// Disables and unregisters an instance, allowing the folder and data to be manipulated manually + /// + /// The instance to detach + /// on success, error message on failure + [OperationContract] + string DetachInstance(string name); + + /// + /// Sets the path to the python 2.7 installation + /// + /// The new path + /// if the path exists, otherwise + [OperationContract] + bool SetPythonPath(string path); + + /// + /// Gets the path to the python 2.7 installation + /// + /// The path to the python 2.7 installation + [OperationContract] + string PythonPath(); + } +} 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/Enumerations.cs b/TGServiceInterface/Enumerations.cs new file mode 100644 index 0000000000..5f65b463b0 --- /dev/null +++ b/TGServiceInterface/Enumerations.cs @@ -0,0 +1,174 @@ +using System; + +namespace TGServiceInterface +{ + /// + /// Description of the connectivity level to an or the + /// + [Flags] + public enum ConnectivityLevel + { + /// + /// The connection could not be made, either a communication error occurred or the specified does not exist + /// + None = 0, + /// + /// The connection could be made + /// + Connected = 1, + /// + /// The connected user is authenticated + /// + Authenticated = 2 | Connected, + /// + /// The connected user is an administrator + /// + Administrator = 4 | Authenticated, + } + + /// + /// 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/InstanceMetadata.cs b/TGServiceInterface/InstanceMetadata.cs new file mode 100644 index 0000000000..5a08e5b355 --- /dev/null +++ b/TGServiceInterface/InstanceMetadata.cs @@ -0,0 +1,32 @@ +using System.Runtime.Serialization; + +namespace TGServiceInterface +{ + /// + /// Metadata about an + /// + [DataContract] + public sealed class InstanceMetadata + { + /// + /// The name of the + /// + [DataMember] + public string Name { get; set; } + /// + /// The path of the + /// + [DataMember] + public string Path { get; set; } + /// + /// Whether or not the is enabled + /// + [DataMember] + public bool Enabled { get; set; } + /// + /// The logging ID of the . Will be 0 if is + /// + [DataMember] + public byte LoggingID { get; set; } + } +} diff --git a/TGServiceInterface/Interface.cs b/TGServiceInterface/Interface.cs new file mode 100644 index 0000000000..57e024527e --- /dev/null +++ b/TGServiceInterface/Interface.cs @@ -0,0 +1,441 @@ +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 +{ + /// + /// Main inteface class for the service + /// + sealed public class Interface : IDisposable + { + /// + /// List of s that can be used with and + /// + public static readonly IList ValidInterfaces = CollectComponents(); + + /// + /// The maximum message size to and from a local server + /// + public const long TransferLimitLocal = Int32.MaxValue; //2GB can't go higher + + /// + /// The maximum message size to and from a remote server + /// + public const long TransferLimitRemote = 10485760; //10 MB + + /// + /// Base name of communication URLs + /// + public const string MasterInterfaceName = "TGStationServerService"; + /// + /// Base name of instance URLs + /// + public const string InstanceInterfaceName = MasterInterfaceName + "/Instance"; + + + /// + /// The name of the current instance in use. Defaults to + /// + public string InstanceName { get; private set; } + + /// + /// If this is set, we will try and connect to an HTTPS server running at this address + /// + public readonly string HTTPSURL; + + /// + /// The port used by the service + /// + public readonly ushort HTTPSPort; + + /// + /// Username for remote operations + /// + readonly string HTTPSUsername; + + /// + /// Password for remote operations + /// + readonly string HTTPSPassword; + + /// + /// Associated list of open s keyed by type name. A in this list may close or fault at any time. Must be locked before being accessed + /// + 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() + { + var ServiceComponent = typeof(ITGSService); //this is special + //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 == ServiceComponent.Namespace + && t.GetCustomAttribute(typeof(ServiceContractAttribute)) != null + && t != ServiceComponent + 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) => + { + string ErrorMessage; + switch (error) + { + case SslPolicyErrors.None: + return true; + case SslPolicyErrors.RemoteCertificateChainErrors: + ErrorMessage = "There are certificate chain errors."; + break; + case SslPolicyErrors.RemoteCertificateNameMismatch: + ErrorMessage = "The certificate name does not match."; + break; + case SslPolicyErrors.RemoteCertificateNotAvailable: + ErrorMessage = "The certificate doesn't exist in the trust store."; + break; + default: + ErrorMessage = "An unknown error occurred."; + break; + } + ErrorMessage = String.Format("The server's certificate failed to verify! Error: {0} Cert: {1}", ErrorMessage, cert.ToString()); + return handler(ErrorMessage); + }; + } + + /// + /// Construct an for a local connection + /// + public Interface() { } + + /// + /// Construct an for a remote connection + /// + /// 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 Interface(string address, ushort port, string username, string password) + { + HTTPSURL = address; + HTTPSPort = port; + HTTPSUsername = username; + HTTPSPassword = password; + } + + /// + /// Constructs an that connects to the same as some + /// + /// + public Interface(Interface other) : this(other.HTTPSURL, other.HTTPSPort, other.HTTPSUsername, other.HTTPSPassword) { } + + /// + /// Targets as the instance to use with . Closes all connections to any previous instance + /// + /// The name of the instance to connect to + /// If set to , skips the connectivity and authentication checks, sets , and returns + /// The apporopriate + public ConnectivityLevel ConnectToInstance(string instanceName = null, bool skipChecks = false) + { + if (instanceName == null) + instanceName = InstanceName; + if (!skipChecks && !ConnectionStatus().HasFlag(ConnectivityLevel.Connected)) + return ConnectivityLevel.None; + var prevInstance = InstanceName; + if (prevInstance != instanceName) + CloseAllChannels(false); + InstanceName = instanceName; + if (skipChecks) + return ConnectivityLevel.Connected; + try + { + GetComponent().VerifyConnection(); + } + catch + { + InstanceName = prevInstance; + return ConnectivityLevel.None; + } + try + { + GetComponent().ServerDirectory(); + } + catch + { + return ConnectivityLevel.Connected; + } + try + { + GetComponent().GetCurrentAuthorizedGroup(); + return ConnectivityLevel.Administrator; + } + catch + { + return ConnectivityLevel.Authenticated; + } + } + + /// + /// Checks if the is setup for a remote connection + /// + public bool IsRemoteConnection { get { return HTTPSURL != null; } } + + /// + /// Closes all s stored in and clears it + /// + /// If set to , doesn't clear the channels that are used by + void CloseAllChannels(bool includingRoot) + { + string[] RootThings = { typeof(ITGSService).Name, 'S' + typeof(ITGConnectivity).Name }; + lock (ChannelFactoryCache) + { + foreach (var I in ChannelFactoryCache) + { + if (RootThings.Contains(I.Key)) + continue; + var cf = I.Value; + try + { + cf.Closed += ChannelFactory_Closed; + cf.Close(); + } + catch + { + cf.Abort(); + } + ChannelFactoryCache.Remove(I); + } + } + } + + /// + /// 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 bool VersionMismatch(out string errorMessage) + { + var splits = GetService().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.Major != ours.Major || theirs.Minor != ours.Minor || theirs.Revision != ours.Revision) //don't care about the patch level + { + errorMessage = String.Format("Version mismatch between interface version ({0}) and service version ({1}). Some functionality may crash this program.", ours, theirs); + return true; + } + errorMessage = null; + return false; + } + + /// + /// Disposes a closed + /// + /// The channel factory that was closed + /// The event arguments + static void ChannelFactory_Closed(object sender, EventArgs e) + { + (sender as IDisposable).Dispose(); + } + + /// + /// Returns the requested component for the instance . This does not guarantee a successful connection. s created this way are recycled for minimum latency and bandwidth usage + /// + /// The component to retrieve + /// The correct component + public T GetComponent() + { + var ToT = typeof(T); + if (!ValidInterfaces.Contains(ToT) && ToT != typeof(ITGSService)) + throw new Exception("Invalid type!"); + return GetComponentImpl(true); + } + + T GetComponentImpl(bool useInstanceName) + { + if (useInstanceName & InstanceName == null) + throw new Exception("Instance not selected!"); + var actualToT = typeof(T); + var tot = actualToT.Name; + if (actualToT == typeof(ITGConnectivity) && !useInstanceName) + tot = 'S' + tot; + ChannelFactory cf; + + lock (ChannelFactoryCache) + { + if (ChannelFactoryCache.ContainsKey(tot)) + try + { + cf = ((ChannelFactory)ChannelFactoryCache[tot]); + if (cf.State != CommunicationState.Opened) + throw new Exception(); + return cf.CreateChannel(); + } + catch + { + ChannelFactoryCache[tot].Abort(); + ChannelFactoryCache.Remove(tot); + } + cf = CreateChannel(useInstanceName ? InstanceName : null); + ChannelFactoryCache[tot] = cf; + } + return cf.CreateChannel(); + } + + /// + /// Returns the component for the service + /// + /// The component for the service + public ITGSService GetService() + { + return GetComponentImpl(false); + } + + /// + /// 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 + ChannelFactory CreateChannel(string instanceName) + { + var accessPath = instanceName == null ? MasterInterfaceName : String.Format("{0}/{1}", InstanceInterfaceName, instanceName); + + var InterfaceName = typeof(T).Name; + if (!IsRemoteConnection) + { + var res2 = new ChannelFactory( + new NetNamedPipeBinding { SendTimeout = new TimeSpan(0, 0, 30), MaxReceivedMessageSize = TransferLimitLocal }, new EndpointAddress(String.Format("net.pipe://localhost/{0}/{1}", accessPath, InterfaceName))); //10 megs + res2.Credentials.Windows.AllowedImpersonationLevel = TokenImpersonationLevel.Impersonation; + return res2; + } + + //okay we're going over + var binding = new WSHttpBinding() + { + SendTimeout = new TimeSpan(0, 0, 40), + MaxReceivedMessageSize = TransferLimitRemote + }; + var requireAuth = InterfaceName != typeof(ITGConnectivity).Name; + binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None; + binding.Security.Mode = requireAuth ? SecurityMode.TransportWithMessageCredential : SecurityMode.Transport; //do not require auth for a connectivity check + binding.Security.Message.ClientCredentialType = requireAuth ? MessageCredentialType.UserName : MessageCredentialType.None; + var address = new EndpointAddress(String.Format("https://{0}:{1}/{2}/{3}", HTTPSURL, HTTPSPort, accessPath, InterfaceName)); + var res = new ChannelFactory(binding, address); + if (requireAuth) + { + res.Credentials.UserName.UserName = HTTPSUsername; + res.Credentials.UserName.Password = HTTPSPassword; + } + return res; + } + + /// + /// Used to test if the is avaiable on the target machine. Note that state can change at any time and any call into the may throw an exception because of communcation errors + /// + /// on successful connection, error message on failure + public ConnectivityLevel ConnectionStatus() + { + return ConnectionStatus(out string unused); + } + + /// + /// Used to test if the is avaiable on the target machine. Note that state can change at any time and any call into the may throw an exception because of communcation errors + /// + /// String of the error that prevented an elevated connectivity level + /// The apporopriate + public ConnectivityLevel ConnectionStatus(out string error) + { + try + { + GetComponentImpl(false).VerifyConnection(); + } + catch (CommunicationException e) + { + error = e.ToString(); + return ConnectivityLevel.None; + } + var service = GetService(); + try + { + service.Version(); + } + catch(Exception e) + { + error = e.ToString(); + return ConnectivityLevel.Connected; + } + try + { + // TODO + + error = null; + return ConnectivityLevel.Administrator; + } + catch(Exception e) + { + error = e.ToString(); + return ConnectivityLevel.Authenticated; + } + } + + #region IDisposable Support + /// + /// To detect redundant calls + /// + private bool disposedValue = false; + + /// + /// Implements the pattern. Calls + /// + /// if was called manually, if it was from the finalizer + void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + CloseAllChannels(true); + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + disposedValue = true; + } + } + + // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. + // ~Interface() { + // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + // Dispose(false); + // } + + /// + /// Implements the pattern + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + // TODO: uncomment the following line if the finalizer is overridden above. + // GC.SuppressFinalize(this); + } + #endregion + } +} diff --git a/TGServiceInterface/Interop.cs b/TGServiceInterface/Interop.cs deleted file mode 100644 index e98a947acf..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 = Server.CreateChannel(); - try - { - channel.CreateChannel().InteropMessage(String.Join(" ", args)); - } - catch { } - Server.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/Server.cs b/TGServiceInterface/Server.cs deleted file mode 100644 index a0bf48284b..0000000000 --- a/TGServiceInterface/Server.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Net; -using System.Net.Security; -using System.Security.Principal; -using System.ServiceModel; -namespace TGServiceInterface -{ - public class Server - { - /// - /// List of types that can be used with GetComponen - /// - public static readonly IList ValidInterfaces = new List { typeof(ITGByond), typeof(ITGChat), typeof(ITGCompiler), typeof(ITGConfig), typeof(ITGDreamDaemon), typeof(ITGRepository), typeof(ITGSService), typeof(ITGConnectivity), typeof(ITGAdministration), typeof(ITGInterop) }; - - /// - /// The maximum message size to and from a local server - /// - public static readonly 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 - - /// - /// Base name of the communication pipe - /// they are formatted as MasterPipeName/ComponentName - /// - public static string MasterInterfaceName = "TGStationServerService"; - - /// - /// If this is set, we will try and connect to an HTTPS server running at this address - /// - static string HTTPSURL; - - /// - /// The port used by the service - /// - static ushort HTTPSPort = 38607; - - /// - /// Username for remote operations - /// - static string HTTPSUsername; - - /// - /// Password for remote operations - /// - static string HTTPSPassword; - - static Dictionary ChannelFactoryCache = new Dictionary(); - public static void SetBadCertificateHandler(Func handler) - { - ServicePointManager.ServerCertificateValidationCallback = (sender, cert, chain, error) => - { - string ErrorMessage; - switch (error) - { - case SslPolicyErrors.None: - return true; - case SslPolicyErrors.RemoteCertificateChainErrors: - ErrorMessage = "There are certificate chain errors."; - break; - case SslPolicyErrors.RemoteCertificateNameMismatch: - ErrorMessage = "The certificate name does not match."; - break; - case SslPolicyErrors.RemoteCertificateNotAvailable: - ErrorMessage = "The certificate doesn't exist in the trust store."; - break; - default: - ErrorMessage = "An unknown error occurred."; - break; - } - ErrorMessage = String.Format("The certificate failed to verify for {0}:{1}. {2} {3}", HTTPSURL, HTTPSPort, ErrorMessage, cert.ToString()); - return handler(ErrorMessage); - }; - } - - /// - /// Set the interface to look for services on the current computer - /// - public static void MakeLocalConnection() - { - HTTPSURL = null; - HTTPSPassword = null; - ClearCachedChannels(); - } - - static void ClearCachedChannels() - { - foreach (var I in ChannelFactoryCache) - CloseChannel(I.Value); - ChannelFactoryCache.Clear(); - } - - public static bool VersionMismatch(out string errorMessage) - { - var splits = Server.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) - { - errorMessage = String.Format("Version mismatch between interface version ({0}) and service version ({1}). Some functionality may crash this program.", ours, theirs); - return true; - } - errorMessage = null; - return false; - } - - /// - /// Set the interface to look for services on a remote computer - /// - /// - /// - public static void SetRemoteLoginInformation(string address, ushort port, string username, string password) - { - HTTPSURL = address; - HTTPSPort = port; - HTTPSUsername = username; - HTTPSPassword = password; - ClearCachedChannels(); - } - - public static void CloseChannel(ChannelFactory cf) - { - try - { - cf.Close(); - } - catch - { - cf.Abort(); - } - } - - /// - /// Returns the requested server component interface. This does not guarantee a successful connection - /// - /// The type of the component to retrieve - /// The correct component - public static T GetComponent() - { - var tot = typeof(T); - ChannelFactory cf; - - lock (ChannelFactoryCache) - { - if (ChannelFactoryCache.ContainsKey(tot)) - try - { - cf = ((ChannelFactory)ChannelFactoryCache[tot]); - if (cf.State != CommunicationState.Opened) - throw new Exception(); - return cf.CreateChannel(); - } - catch - { - ChannelFactoryCache[tot].Abort(); - ChannelFactoryCache.Remove(tot); - } - cf = CreateChannel(); - ChannelFactoryCache[tot] = cf; - } - return cf.CreateChannel(); - } - - public static ChannelFactory CreateChannel() - { - var ToT = typeof(T); - if (!ValidInterfaces.Contains(ToT)) - throw new Exception("Invalid type!"); - var InterfaceName = typeof(T).Name; - if (HTTPSURL == null) - { - var res2 = new ChannelFactory( - new NetNamedPipeBinding { SendTimeout = new TimeSpan(0, 0, 30), MaxReceivedMessageSize = TransferLimitLocal }, new EndpointAddress(String.Format("net.pipe://localhost/{0}/{1}", MasterInterfaceName, InterfaceName))); //10 megs - res2.Credentials.Windows.AllowedImpersonationLevel = TokenImpersonationLevel.Impersonation; - return res2; - } - - //okay we're going over - var binding = new WSHttpBinding() - { - SendTimeout = new TimeSpan(0, 0, 40), - MaxReceivedMessageSize = TransferLimitRemote - }; - var requireAuth = InterfaceName != typeof(ITGConnectivity).Name; - binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None; - binding.Security.Mode = requireAuth ? SecurityMode.TransportWithMessageCredential : SecurityMode.Transport; //do not require auth for a connectivity check - binding.Security.Message.ClientCredentialType = requireAuth ? MessageCredentialType.UserName : MessageCredentialType.None; - var address = new EndpointAddress(String.Format("https://{0}:{1}/{2}/{3}", HTTPSURL, HTTPSPort, MasterInterfaceName, InterfaceName)); - var res = new ChannelFactory(binding, address); - if (requireAuth) - { - res.Credentials.UserName.UserName = HTTPSUsername; - res.Credentials.UserName.Password = HTTPSPassword; - } - return res; - } - - /// - /// 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 - public static string VerifyConnection() - { - try - { - GetComponent().VerifyConnection(); - return null; - } - catch (Exception e) - { - return e.ToString(); - } - } - - /// - /// As opposed to VerifyConnection(), this check user credentials - /// Requires a prior call to - /// - /// true if credentials are valid, false otherwise - public static bool Authenticate() - { - try - { - GetComponent().Version(); - return true; - } - catch - { - return false; - } - } - - /// - /// As opposed to Authentication() this returns true if the current login can use the interface. - /// Requires a prior call to - /// - /// true if the connection may use the interface, false otherwise - public static bool AuthenticateAdmin() - { - try - { - GetComponent().GetCurrentAuthorizedGroup(); - return true; - } - catch - { - return false; - } - } - } -} diff --git a/TGServiceInterface/Service.cs b/TGServiceInterface/Service.cs deleted file mode 100644 index 4ccaa040bc..0000000000 --- a/TGServiceInterface/Service.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.ServiceModel; - -namespace TGServiceInterface -{ - /// - /// Interface for managing the service - /// - [ServiceContract] - public interface ITGSService - { - - /// - /// Next stop of the service will not close DD and sets a flag for it to reattach once it restarts - /// - [OperationContract] - void PrepareForUpdate(); - - /// - /// Retrieve's the service's version - /// - /// The service's version - [OperationContract] - string Version(); - } -} diff --git a/TGServiceInterface/TGServiceInterface.csproj b/TGServiceInterface/TGServiceInterface.csproj index a098c77c14..5e9befc3be 100644 --- a/TGServiceInterface/TGServiceInterface.csproj +++ b/TGServiceInterface/TGServiceInterface.csproj @@ -15,29 +15,28 @@ tgs.ico - + true - bin\x86\Debug\ + bin\Debug\ DEBUG;TRACE full - x86 + AnyCPU prompt MinimumRecommendedRules.ruleset - - bin\x86\Release\ + + bin\Release\ TRACE + bin\x86\Release\TGServiceInterface.xml true + true pdbonly - x86 + AnyCPU prompt MinimumRecommendedRules.ruleset + false - - ..\packages\UnmanagedExports.1.2.7\lib\net\RGiesecke.DllExport.Metadata.dll - False - @@ -45,28 +44,33 @@ - - + + + - - - - - + + + + + + + + - - + + + + - - + + - + - \ No newline at end of file diff --git a/TGServiceInterface/TGServiceInterface.nuspec b/TGServiceInterface/TGServiceInterface.nuspec new file mode 100644 index 0000000000..43fb6d2479 --- /dev/null +++ b/TGServiceInterface/TGServiceInterface.nuspec @@ -0,0 +1,19 @@ + + + + $id$ + $version$ + Cyberboss + https://github.com/tgstation/tgstation-server/blob/master/LICENSE + https://github.com/tgstation/tgstation-server + https://tgstation.github.io/tgstation-server/tgs.ico + false + Interface for managing /tg/station server + Copyright 2017 + https://github.com/tgstation/tgstation-server/releases/tag/tgstation-server-v$version$ + tgstation game-server byond service toolset wcf ss13 client-lib tgstation-server api wcf-service soap + + + + + diff --git a/TGServiceInterface/packages.config b/TGServiceInterface/packages.config deleted file mode 100644 index bfd9f475da..0000000000 --- a/TGServiceInterface/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/TGServiceTests/Interface/TestHelpers.cs b/TGServiceTests/Interface/TestHelpers.cs new file mode 100644 index 0000000000..6a0086bd7c --- /dev/null +++ b/TGServiceTests/Interface/TestHelpers.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace TGServiceInterface.Tests +{ + /// + /// Tests for + /// + [TestClass] + public class TestHelpers + { + /// + /// Sample cleartext + /// + const string PlainText = "According to all known laws of aviation, there is no way a bee should be able to fly. Its wings are too small to get its fat little body off the ground. The bee, of course, flies anyway because bees don't care what humans think is impossible."; + + /// + /// Run assertions for a successful call to + /// + /// The out string for the entropy parameter of + /// The result of with as a parameter + string AssertEncryptData(out string entropy) + { + var result = Helpers.EncryptData(PlainText, out entropy); + Assert.AreNotEqual(PlainText, entropy); + Assert.AreNotEqual(PlainText, result); + Assert.AreNotEqual(result, entropy); + Assert.IsFalse(String.IsNullOrWhiteSpace(result)); + Assert.IsFalse(String.IsNullOrWhiteSpace(entropy)); + return result; + } + + /// + /// Tests that can execute successfully + /// + [TestMethod] + public void TestEncryptDataWorks() + { + AssertEncryptData(out string entropy); + } + + /// + /// Tests that can execute successfully + /// + [TestMethod] + public void TestDecryptDataWorks() + { + var result = AssertEncryptData(out string entropy); + var decrypted = Helpers.DecryptData(result, entropy); + Assert.AreEqual(decrypted, PlainText); + } + } +} diff --git a/TGServiceTests/Interface/TestInterface.cs b/TGServiceTests/Interface/TestInterface.cs new file mode 100644 index 0000000000..dafa7270db --- /dev/null +++ b/TGServiceTests/Interface/TestInterface.cs @@ -0,0 +1,80 @@ +using System; +using System.Net; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace TGServiceInterface.Tests +{ + /// + /// Tests for + /// + [TestClass] + public class TestInterface + { + /// + /// Test that can execute successfully + /// + [TestMethod] + public void TestSetBadCertificateHandler() + { + Func func = (message) => + { + Assert.IsFalse(String.IsNullOrWhiteSpace(message)); + return true; + }; + Interface.SetBadCertificateHandler(func); + } + + /// + /// Test that properly sets + /// + [TestMethod] + public void TestBadCertificateHandler() + { + var ran = false; + Interface.SetBadCertificateHandler(_ => + { + ran = true; + return true; + }); + ServicePointManager.ServerCertificateValidationCallback(this, new System.Security.Cryptography.X509Certificates.X509Certificate(), new System.Security.Cryptography.X509Certificates.X509Chain(), System.Net.Security.SslPolicyErrors.RemoteCertificateChainErrors); + Assert.IsTrue(ran); + } + + /// + /// Creates a remote configured pointing at an invalid address + /// + /// The created + Interface CreateFakeRemoteInterface() + { + return new Interface("some.fake.url.420", 34752, "user", "password"); + } + + /// + /// Test that can execute successfully and creates a local connection + /// + [TestMethod] + public void TestLocalInstantiation() + { + Assert.IsFalse(new Interface().IsRemoteConnection); + } + + /// + /// Test that can execute successfully + /// + [TestMethod] + public void TestRemoteInstatiation() + { + Assert.IsTrue(CreateFakeRemoteInterface().IsRemoteConnection); + } + + [TestMethod] + public void TestCopyRemoteInterface() + { + var first = CreateFakeRemoteInterface(); + var second = new Interface(first); + Assert.AreEqual(first.HTTPSURL, second.HTTPSURL); + Assert.AreEqual(first.HTTPSPort, second.HTTPSPort); + Assert.IsTrue(second.IsRemoteConnection); + } + } +} diff --git a/TGServiceTests/Properties/AssemblyInfo.cs b/TGServiceTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..56c3be7ba4 --- /dev/null +++ b/TGServiceTests/Properties/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("TGStation Server Test Suite")] +[assembly: AssemblyDescription("Unit tests for the TGStation Server suite")] + +[assembly: ComVisible(false)] + +[assembly: Guid("fb693ffb-17e3-4e84-8cbf-6ffa9c8fd971")] + diff --git a/TGServiceTests/Service/ServiceAccessor.cs b/TGServiceTests/Service/ServiceAccessor.cs new file mode 100644 index 0000000000..97c20d0d78 --- /dev/null +++ b/TGServiceTests/Service/ServiceAccessor.cs @@ -0,0 +1,27 @@ + + +namespace TGServerService.Tests +{ + /// + /// For accessing service control methods of + /// + class ServiceAccessor : Service + { + /// + /// Fake a start up + /// + /// Fake commandline parameters passed to + public void FakeStart(string[] args) + { + OnStart(args); + } + + /// + /// Fake a shutdown + /// + public void FakeStop() + { + OnStop(); + } + } +} diff --git a/TGServiceTests/Service/TestInstanceConfig.cs b/TGServiceTests/Service/TestInstanceConfig.cs new file mode 100644 index 0000000000..3d49c9a24e --- /dev/null +++ b/TGServiceTests/Service/TestInstanceConfig.cs @@ -0,0 +1,62 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.IO; +using TGServiceTests; + +namespace TGServerService.Tests +{ + /// + /// Tests for + /// + [TestClass] + public class TestInstanceConfig : TempDirectoryRequiredTest + { + /// + /// The path to the JSON at + /// + string InstanceJSONPath { get { return Path.Combine(TempPath, InstanceConfig.JSONFilename); } } + + /// + /// Creates a default at + /// + /// + InstanceConfig CreateTempConfig() + { + return new InstanceConfig(TempPath); + } + + /// + /// Test that can execute successfully and doesn't automatically save + /// + [TestMethod] + public void TestCreate() + { + var IC = CreateTempConfig(); + Assert.IsFalse(File.Exists(InstanceJSONPath)); + } + + /// + /// Test that works correctly + /// + [TestMethod] + public void TestSave() + { + var IC = CreateTempConfig(); + IC.Save(); + Assert.IsTrue(File.Exists(InstanceJSONPath)); + } + + /// + /// Test that works correctly + /// + [TestMethod] + public void TestLoad() + { + var IC = CreateTempConfig(); + var name = "asdf"; + IC.Name = name; + IC.Save(); + var IC2 = InstanceConfig.Load(TempPath); + Assert.AreEqual(name, IC2.Name); + } + } +} diff --git a/TGServiceTests/Service/TestServerInstance.cs b/TGServiceTests/Service/TestServerInstance.cs new file mode 100644 index 0000000000..27b79b6c4b --- /dev/null +++ b/TGServiceTests/Service/TestServerInstance.cs @@ -0,0 +1,21 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TGServiceTests; + +namespace TGServerService.Tests +{ + /// + /// Tests for + /// + [TestClass] + public class TestServerInstance : TempDirectoryRequiredTest + { + /// + /// Test a can be created and destroyed successfully with a basic + /// + [TestMethod] + public void TestBasicInstantiation() + { + new ServerInstance(new InstanceConfig(TempPath), 1).Dispose(); + } + } +} diff --git a/TGServiceTests/Service/TestService.cs b/TGServiceTests/Service/TestService.cs new file mode 100644 index 0000000000..43d6f277f6 --- /dev/null +++ b/TGServiceTests/Service/TestService.cs @@ -0,0 +1,47 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TGServiceTests; + +namespace TGServerService.Tests +{ + /// + /// Tests for + /// + [TestClass] + public class TestService : TempDirectoryRequiredTest + { + /// + /// Test can execute successfully + /// + [TestMethod] + public void TestInstantiation() + { + new Service().Dispose(); + } + + /// + /// Test and can execute successfully + /// + [TestMethod] + public void TestStartupAndShutdown() + { + using (var S = new ServiceAccessor()) + { + S.FakeStart(new string[] { }); + S.FakeStop(); + } + } + + /// + /// Test and can execute successfully with a commandline port override + /// + [TestMethod] + public void TestCommandLinePortSet() + { + using (var S = new ServiceAccessor()) + { + S.FakeStart(new string[] { "-port", "36785" }); + S.FakeStop(); + } + } + } +} diff --git a/TGServiceTests/TGServiceTests.csproj b/TGServiceTests/TGServiceTests.csproj new file mode 100644 index 0000000000..23dccc15c1 --- /dev/null +++ b/TGServiceTests/TGServiceTests.csproj @@ -0,0 +1,87 @@ + + + + + Debug + AnyCPU + {FB693FFB-17E3-4E84-8CBF-6FFA9C8FD971} + Library + Properties + TGServiceTests + TGServiceTests + v4.6.1 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\MSTest.TestFramework.1.1.18\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + ..\packages\MSTest.TestFramework.1.1.18\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + + + + + + + + + Component + + + + + + + + + + + + + {f32eda25-0855-411c-af5e-f0d042917e2d} + TGServerService + + + {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/TGServiceTests/TempDirectoryRequiredTest.cs b/TGServiceTests/TempDirectoryRequiredTest.cs new file mode 100644 index 0000000000..a4ef44cf09 --- /dev/null +++ b/TGServiceTests/TempDirectoryRequiredTest.cs @@ -0,0 +1,41 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.IO; + +namespace TGServiceTests +{ + /// + /// To be the parent of test classes that required a temporary directory + /// + public class TempDirectoryRequiredTest + { + /// + /// The path to the temporary directory + /// + protected string TempPath; + + /// + /// Construct a + /// + internal TempDirectoryRequiredTest() { } + + /// + /// Setup + /// + [TestInitialize] + public void Setup() + { + TempPath = Path.GetTempFileName(); + File.Delete(TempPath); + Directory.CreateDirectory(TempPath); + } + + /// + /// Cleanup + /// + [TestCleanup] + public void Cleanup() + { + Directory.Delete(TempPath, true); + } + } +} diff --git a/TGServiceTests/packages.config b/TGServiceTests/packages.config new file mode 100644 index 0000000000..d8c1b9099c --- /dev/null +++ b/TGServiceTests/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/TGStationServer3.sln b/TGStationServer3.sln index 48358be2aa..d0be8fd839 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.27004.2006 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,6 +70,26 @@ 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TGDreamDaemonBridge", "TGDreamDaemonBridge\TGDreamDaemonBridge.csproj", "{9A01EF03-8EAE-45CB-8B87-4A17BD904557}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TGServiceTests", "TGServiceTests\TGServiceTests.csproj", "{FB693FFB-17E3-4E84-8CBF-6FFA9C8FD971}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -94,18 +108,24 @@ Global {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 + {AC4E7E8B-F83A-481C-A8B0-8FA4E8AE59AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC4E7E8B-F83A-481C-A8B0-8FA4E8AE59AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC4E7E8B-F83A-481C-A8B0-8FA4E8AE59AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC4E7E8B-F83A-481C-A8B0-8FA4E8AE59AB}.Release|Any CPU.Build.0 = Release|Any CPU {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 + {9A01EF03-8EAE-45CB-8B87-4A17BD904557}.Debug|Any CPU.ActiveCfg = Debug|x86 + {9A01EF03-8EAE-45CB-8B87-4A17BD904557}.Debug|Any CPU.Build.0 = Debug|x86 + {9A01EF03-8EAE-45CB-8B87-4A17BD904557}.Release|Any CPU.ActiveCfg = Release|x86 + {9A01EF03-8EAE-45CB-8B87-4A17BD904557}.Release|Any CPU.Build.0 = Release|x86 + {FB693FFB-17E3-4E84-8CBF-6FFA9C8FD971}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB693FFB-17E3-4E84-8CBF-6FFA9C8FD971}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB693FFB-17E3-4E84-8CBF-6FFA9C8FD971}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB693FFB-17E3-4E84-8CBF-6FFA9C8FD971}.Release|Any CPU.Build.0 = Release|Any CPU 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 +# , /