diff --git a/TGServerService/InstanceConfig.cs b/TGServerService/InstanceConfig.cs index 429ba1b8e6..3e2a075a16 100644 --- a/TGServerService/InstanceConfig.cs +++ b/TGServerService/InstanceConfig.cs @@ -11,7 +11,7 @@ namespace TGServerService /// //tell javascriptserializer to ignore these fields [ScriptIgnore] - const string JSONFilename = "Instance.json"; + public const string JSONFilename = "Instance.json"; /// /// The current version of the config /// diff --git a/TGServerService/Program.cs b/TGServerService/Program.cs index cd7e73e652..a9c208a629 100644 --- a/TGServerService/Program.cs +++ b/TGServerService/Program.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.ServiceProcess; namespace TGServerService { @@ -9,7 +10,10 @@ namespace TGServerService /// /// Entry point to the program /// - static void Main() => Service.Launch(); + static void Main() { + using (var S = new Service()) + ServiceBase.Run(S); + } /// /// Copy a file from to , but first ensure the destination directory exists 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/Service.cs b/TGServerService/Service.cs index 7792e7ab45..b9d8547453 100644 --- a/TGServerService/Service.cs +++ b/TGServerService/Service.cs @@ -1,696 +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)] - sealed 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); - } - - /// - /// Constructs and runs a . Do not add any code that might need debugging here as it's near impossible to get Windows to debug it properly without timing out - /// - public static void Launch() - { - if (ActiveService != null) - throw new Exception("There is already a Service instance running!"); - ActiveService = new Service(); - Run(ActiveService); - } - - /// - /// 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 the service name. Do not add any more code due to the reasons outlined in - /// - Service() - { - ServiceName = "TG Station Server"; - } - - /// - /// 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; - } - } - } -} +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/TGServiceInterface/ChatSetupInfo.cs b/TGServiceInterface/ChatSetupInfo.cs index ddbe90156d..c34d6aea8e 100644 --- a/TGServiceInterface/ChatSetupInfo.cs +++ b/TGServiceInterface/ChatSetupInfo.cs @@ -26,7 +26,7 @@ namespace TGServiceInterface /// protected const int BaseIndex = 8; /// - /// Set to if a child constructor should use the baseInfo parameter of to initialize it's property fields, otherwise + /// Set to if a child constructor should use the baseInfo parameter of to initialize it's property fields, otherwise /// protected readonly bool InitializeFields; @@ -34,14 +34,15 @@ namespace TGServiceInterface /// Raw access to the underlying data /// [DataMember] - public IList DataFields { get; protected set; } - + 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 ChatSetupInfo(ChatSetupInfo baseInfo, int numFields) + protected internal ChatSetupInfo(ChatProvider provider, ChatSetupInfo baseInfo, int numFields) { numFields += BaseIndex; InitializeFields = baseInfo == null || baseInfo.DataFields.Count != numFields; @@ -62,23 +63,31 @@ namespace TGServiceInterface } else DataFields = baseInfo.DataFields; + Provider = provider; + Specialize(true); //to check we have a valid provider } /// /// Recreates as the correct child /// + /// If , is returned provided is a valid /// A new based on the type - ChatSetupInfo Specialize() + ChatSetupInfo Specialize(bool checkOnly) { switch (Provider) { case ChatProvider.IRC: - return new IRCSetupInfo(this); + if (!checkOnly) + return new IRCSetupInfo(this); + break; case ChatProvider.Discord: - return new DiscordSetupInfo(this); + if (!checkOnly) + return new DiscordSetupInfo(this); + break; default: throw new Exception("Invalid provider!"); } + return null; } /// @@ -88,7 +97,7 @@ namespace TGServiceInterface /// The formatted protected virtual string SanitizeChannelName(string channel) { - return Specialize().SanitizeChannelName(channel); + return Specialize(false).SanitizeChannelName(channel); } /// @@ -115,6 +124,7 @@ namespace TGServiceInterface public ChatSetupInfo(IList DeserializedData) { DataFields = DeserializedData; + Specialize(false); //ensure provider type is valid } /// /// The list of admin entries @@ -211,25 +221,24 @@ 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 IRCSetupInfo(ChatSetupInfo baseInfo = null) : base(baseInfo, FieldsLen) + public IRCSetupInfo(ChatSetupInfo baseInfo = null) : base(ChatProvider.IRC, baseInfo, FieldsLen) { - Provider = ChatProvider.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; } /// @@ -297,16 +306,16 @@ namespace TGServiceInterface 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 DiscordSetupInfo(ChatSetupInfo baseInfo = null) : base(baseInfo, FieldsLen) + public DiscordSetupInfo(ChatSetupInfo baseInfo = null) : base(ChatProvider.Discord, baseInfo, FieldsLen) { - Provider = ChatProvider.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) 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 00d427c311..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.16 +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 @@ -88,6 +88,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{287B 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 @@ -120,6 +122,10 @@ Global {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/appveyor.yml b/appveyor.yml index a7785a0bf0..c1c67d718f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,52 +1,52 @@ -pull_requests: - do_not_increment_build_number: true -environment: - repo_token: - secure: lJNGAXwiB5HlWdthz3K4PetqpTG5IEAyRgKaiKxFMQ8HW8CcOjRtB97B05op7BsK -skip_branch_with_pr: true -image: Visual Studio 2017 -configuration: Release -shallow_clone: true -artifacts: -- path: TGS3-Server-v*.exe - name: TGS3Server -- path: MD5-SHA1-Server-v*.txt - name: MD5SHA1Server -- path: TGS3-Client-v*.zip - name: TGS3Client -- path: MD5-SHA1-Client-v*.txt - name: MD5SHA1Client -cache: - - packages -> **\packages.config - - C:\ProgramData\chocolatey\bin -> appveyor.yml - - C:\ProgramData\chocolatey\lib -> appveyor.yml -install: - - choco install fciv doxygen.portable graphviz.portable -before_build: - - nuget restore TGStationServer3.sln -build: - project: TGStationServer3.sln - parallel: true - verbosity: minimal - publish_nuget: true -after_build: - - ps: .\Tools\TGS3Build.ps1 - - ps: if($env:APPVEYOR_REPO_COMMIT_MESSAGE -match "\[TGSDeploy\]"){$env:TGSDeploy = "Do it."} - - ps: $env:TGSVersion = [System.Diagnostics.FileVersionInfo]::GetVersionInfo("$env:APPVEYOR_BUILD_FOLDER/TGServerService/bin/Release/TGServerService.exe").FileVersion -deploy: - - provider: GitHub - release: "tgstation-server-v$(TGSVersion)" - description: 'The /tg/station server suite' - auth_token: - secure: lJNGAXwiB5HlWdthz3K4PetqpTG5IEAyRgKaiKxFMQ8HW8CcOjRtB97B05op7BsK - artifact: TGS3Server,MD5SHA1Server,TGS3Client,MD5SHA1Client - draft: false - on: - TGSDeploy: "Do it." - - provider: NuGet - api_key: - secure: bedsYuLMqGREzkVkJqRx+BTMgOvDO76tgaNc8sW5E3Ao6iw8oGHdJ/BZov8y0iKa - skip_symbols: true - artifact: /.*\.nupkg/ - on: - TGSDeploy: "Do it." +pull_requests: + do_not_increment_build_number: true +environment: + repo_token: + secure: lJNGAXwiB5HlWdthz3K4PetqpTG5IEAyRgKaiKxFMQ8HW8CcOjRtB97B05op7BsK +skip_branch_with_pr: true +image: Visual Studio 2017 +configuration: Release +shallow_clone: true +artifacts: +- path: TGS3-Server-v*.exe + name: TGS3Server +- path: MD5-SHA1-Server-v*.txt + name: MD5SHA1Server +- path: TGS3-Client-v*.zip + name: TGS3Client +- path: MD5-SHA1-Client-v*.txt + name: MD5SHA1Client +cache: + - packages -> **\packages.config + - C:\ProgramData\chocolatey\bin -> appveyor.yml + - C:\ProgramData\chocolatey\lib -> appveyor.yml +install: + - choco install fciv doxygen.portable graphviz.portable +before_build: + - nuget restore TGStationServer3.sln +build: + project: TGStationServer3.sln + parallel: true + verbosity: minimal + publish_nuget: true +after_build: + - ps: .\Tools\TGS3Build.ps1 + - ps: if($env:APPVEYOR_REPO_COMMIT_MESSAGE -match "\[TGSDeploy\]"){$env:TGSDeploy = "Do it."} + - ps: $env:TGSVersion = [System.Diagnostics.FileVersionInfo]::GetVersionInfo("$env:APPVEYOR_BUILD_FOLDER/TGServerService/bin/Release/TGServerService.exe").FileVersion +deploy: + - provider: GitHub + release: "tgstation-server-v$(TGSVersion)" + description: 'The /tg/station server suite' + auth_token: + secure: lJNGAXwiB5HlWdthz3K4PetqpTG5IEAyRgKaiKxFMQ8HW8CcOjRtB97B05op7BsK + artifact: TGS3Server,MD5SHA1Server,TGS3Client,MD5SHA1Client + draft: false + on: + TGSDeploy: "Do it." + - provider: NuGet + api_key: + secure: bedsYuLMqGREzkVkJqRx+BTMgOvDO76tgaNc8sW5E3Ao6iw8oGHdJ/BZov8y0iKa + skip_symbols: true + artifact: /.*\.nupkg/ + on: + TGSDeploy: "Do it."