using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Security.Principal;
using System.ServiceModel;
using System.ServiceModel.Description;
using TGS.Interface;
using TGS.Interface.Components;
using TGS.Server.Security;
namespace TGS.Server
{
///
/// The windows service the application runs as
///
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.Single)]
public sealed class Server : ITGSService, ITGConnectivity, ITGLanding, ITGInstanceManager, IDisposable
{
///
/// The logging ID used for events
///
public const byte LoggingID = 0;
///
/// The directory to use when importing a .NET settings based config
///
public const string MigrationConfigDirectory = "C:\\TGSSettingUpgradeTempDir";
///
/// The service version based on the 's
///
public static readonly string VersionString = String.Format("/tg/station 13 Server v{0}", Assembly.GetExecutingAssembly().GetName().Version);
///
/// Singleton
///
public static ILogger Logger { get; private set; }
///
/// The for the
///
public static ServerConfig Config { get; private set; }
///
/// The directory to load and save s to
///
static readonly string DefaultConfigDirectory = Directory.CreateDirectory(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TGS.Server")).FullName;
///
/// Begins user impersonation to allow proper restricted file access
///
public static void BeginImpersonation()
{
WindowsIdentity.Impersonate(OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Token);
}
///
/// Cancels WCF's user impersonation to allow clean access to writing log files
///
public static void CancelImpersonation()
{
WindowsIdentity.Impersonate(IntPtr.Zero);
}
///
/// 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;
}
///
/// 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();
///
/// Construct a
///
/// Command line arguments for the
/// The to use
public Server(string[] args, ILogger logger)
{
Logger = logger;
Environment.CurrentDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Assembly.GetExecutingAssembly().GetName().Name)).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();
}
///
/// Enumerates configured s. Detaches those that fail to load
///
/// Each configured
IEnumerable GetInstanceConfigs()
{
var pathsToRemove = new List();
lock (this)
{
var IPS = Config.InstancePaths;
foreach (var I in IPS)
{
IInstanceConfig ic;
try
{
ic = InstanceConfig.Load(I);
}
catch (Exception e)
{
Logger.WriteError(String.Format("Unable load instance config at path {0}. Error: {1} Detaching...", I, e.ToString()), EventID.InstanceInitializationFailure, 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)
{
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(DefaultConfigDirectory);
break;
}
}
///
/// Upgrades up the service configuration
///
void SetupConfig()
{
Directory.CreateDirectory(DefaultConfigDirectory);
try
{
Config = ServerConfig.Load(DefaultConfigDirectory);
}
catch
{
try
{
//assume we're upgrading
Config = ServerConfig.Load(MigrationConfigDirectory);
Config.Save(DefaultConfigDirectory);
Helpers.DeleteDirectory(MigrationConfigDirectory);
}
catch
{
//new baby
Config = new ServerConfig();
}
}
}
///
/// Creates the for
///
void SetupService()
{
serviceHost = CreateHost(this, Definitions.MasterInterfaceName);
AddEndpoint(serviceHost, typeof(ITGConnectivity));
AddEndpoint(serviceHost, typeof(ITGInstanceManager));
AddEndpoint(serviceHost, typeof(ITGLanding));
AddEndpoint(serviceHost, typeof(ITGSService));
serviceHost.Authorization.ServiceAuthorizationManager = new RootAuthorizationManager(); //only admins can diddle us
serviceHost.Authentication.ServiceAuthenticationManager = new AuthenticationHeaderDecoder();
}
///
/// 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}", Config.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))
{
Logger.WriteError(String.Format("Instance at {0} has a duplicate name! Detaching...", I.Directory), EventID.InstanceInitializationFailure, LoggingID);
pathsToRemove.Add(I.Directory);
}
if (I.Enabled && SetupInstance(I) == null)
pathsToRemove.Add(I.Directory);
else
seenNames.Add(I.Name);
}
foreach (var I in pathsToRemove)
Config.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(IInstanceConfig config)
{
Instance instance;
string instanceName;
try
{
if (hosts.ContainsKey(config.Directory))
{
var datInstance = ((Instance)hosts[config.Directory].SingletonInstance);
Logger.WriteError(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, LoggingID);
return null;
}
if (!config.Enabled)
return null;
var ID = LockLoggingID();
Logger.WriteInfo(String.Format("Instance {0} ({1}) assigned logging ID {2}", config.Name, config.Directory, ID), EventID.InstanceIDAssigned, ID);
instanceName = config.Name;
instance = new Instance(config, ID);
}
catch (Exception e)
{
Logger.WriteError(String.Format("Unable to start instance at path {0}. Detaching... Error: {1}", config.Directory, e.ToString()), EventID.InstanceInitializationFailure, LoggingID);
return null;
}
var host = CreateHost(instance, String.Format("{0}/{1}", Definitions.InstanceInterfaceName, instanceName));
hosts.Add(instanceName, host);
AddEndpoint(host, typeof(ITGConnectivity));
AddEndpoint(host, typeof(ITGAdministration));
AddEndpoint(host, typeof(ITGByond));
AddEndpoint(host, typeof(ITGChat));
AddEndpoint(host, typeof(ITGCompiler));
AddEndpoint(host, typeof(ITGConfig));
AddEndpoint(host, typeof(ITGDreamDaemon));
AddEndpoint(host, typeof(ITGInstance));
AddEndpoint(host, typeof(ITGInterop));
AddEndpoint(host, typeof(ITGRepository));
host.Authorization.ServiceAuthorizationManager = instance;
host.Authentication.ServiceAuthenticationManager = new AuthenticationHeaderDecoder();
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;
var endpint = host.AddServiceEndpoint(typetype, new NetNamedPipeBinding() { SendTimeout = new TimeSpan(0, 0, 30), MaxReceivedMessageSize = Definitions.TransferLimitLocal }, bindingName);
foreach (OperationDescription od in endpint.Contract.Operations)
od.OperationBehaviors.Add(new AddCallLoggerBehavior());
var httpsBinding = new BasicHttpsBinding()
{
SendTimeout = new TimeSpan(0, 0, 40),
MaxReceivedMessageSize = Definitions.TransferLimitRemote
};
var requireAuth = typetype.Name != typeof(ITGConnectivity).Name;
host.AddServiceEndpoint(typetype, httpsBinding, bindingName);
}
///
/// Shuts down all active s and calls on it's
///
void OnStop()
{
lock (this)
{
try
{
foreach (var I in hosts)
{
var host = I.Value;
var instance = (Instance)host.SingletonInstance;
host.Close();
instance.Dispose();
UnlockLoggingID(instance.LoggingID);
}
}
catch (Exception e)
{
Logger.WriteError(e.ToString(), EventID.ServiceShutdownFail, LoggingID);
}
serviceHost.Close();
}
Config.Save(DefaultConfigDirectory);
}
///
public void VerifyConnection() { }
///
public void PrepareForUpdate()
{
foreach (var I in hosts)
((Instance)I.Value.SingletonInstance).Reattach(false);
}
///
public ushort RemoteAccessPort()
{
return Config.RemoteAccessPort;
}
///
public string SetRemoteAccessPort(ushort port)
{
if (port == 0)
return "Cannot bind to port 0";
Config.RemoteAccessPort = port;
return null;
}
///
public string Version()
{
return VersionString;
}
///
public bool SetPythonPath(string path)
{
if (!Directory.Exists(path))
return false;
Config.PythonPath = Path.GetFullPath(path);
return true;
}
///
public string PythonPath()
{
return Config.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 ? ((Instance)hosts[ic.Name].SingletonInstance).LoggingID : 0)
});
return result;
}
///
public string CreateInstance(string Name, string path)
{
path = Helpers.NormalizePath(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!";
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);
IInstanceConfig ic;
try
{
ic = new InstanceConfig(path)
{
Name = Name
};
Directory.CreateDirectory(path);
ic.Save();
Config.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(IInstanceConfig config)
{
if (!config.Enabled)
return null;
try
{
var host = SetupInstance(config);
if (host != null)
host.Open();
else
lock (this)
Config.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)
{
path = Helpers.NormalizePath(path);
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);
IInstanceConfig 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();
Config.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;
foreach (var ic in GetInstanceConfigs())
if (ic.Name == Name)
{
path = ic.Directory;
ic.Enabled = true;
ic.Save();
return SetupOneInstance(ic);
}
return String.Format("Instance {0} does not exist!", Name);
}
else
{
if (!hostIsOnline)
return null;
var host = hosts[Name];
hosts.Remove(Name);
var inst = (Instance)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
IInstanceConfig 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);
Config.InstancePaths.Remove(path);
return null;
}
}
#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)
{
OnStop();
}
// 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.
// ~Server() {
// // 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
}
}