需求
由于业务要求,我们实现了很多windows服务,但是制作服务并不是一件简单的事情:
1.需要创建独立安装包
2.升级时也需要制作安装包
3.服务间无法相互通信
我们需要一个容器将这些服务包装在一起,将每个服务视作一个子服务,提供方便安装,升级,卸载的功能,监控每个服务的状态,并实现消息总线以方便子服务间通信
概念
我们可以将服务加载到相互独立的应用程序域中, 可以将windows服务管理这部分功能通过操作应用程序域来完成。这样实际上就是要实现管理这些windows服务的容器,这个容器我们称之为HostService。
HostService本身就是一个windows 服务,可以动态加载,升级,卸载子服务
HostService包括以下模块
ServiceManager-加载,卸载子服务,并保存子服务实例
autoupdater-基于squirrel.windows的自动升级模块,子服务从repository中自动下载(支持文件或HTTP协议),根据版本自动升级,支持patch升级
控制面板-用来配置服务运行的参数,比如子服务repository路径,监控子服务运行状态。控制面板和hostservice之间通过thrif通信
热插拔子服务
每个子服务就是.NET的assembly,脱离hostservice可以直接运行。如果运行在hostservice,实现主逻辑的类需要派生于HostServicebase的抽象类并实现以下接口
// base class each plugin must inherit public abstract class HostServiceBase { protected abstract void OnStart(String assemblyLocation); // trigger plugin logic to start protected abstract void OnStop(); // trigger plugin logic to stop protected abstract String GetServiceName();// plugin friendly name protected abstract String GetVersion(); // can be used to store verison of assembly}
HostService加载派生于HostServicebase的类时会自动分配一个messagequeue用于和servicemanager通信,这个messagequeue是hostservice创建的,并通过)子服务程序域传到子服务实例AppDomain.CurrentDomain.SetData
HostMessageQueue
public class HostServiceMessageQueue : MarshalByRefObject { BlockingCollectionmMessageQueue = new BlockingCollection (); public void AddMessage(HostServiceMessage sMsg) { mMessageQueue.Add(sMsg); } public HostServiceMessage TakeMessage() { return mMessageQueue.Take(); } public override object InitializeLifetimeService() { return null; } }
ServiceManager加载子服务的serviceRunner
public class ServiceRunner : MarshalByRefObject { public static int EXECUTION_TIMEOUT = 15; protected HostServiceMessageQueue mMessageQueue = new HostServiceMessageQueue(); protected HostServiceMessageQueue mHostMessageQueue = null; public ServiceRunner() { } public Assembly loadAssembly(String assemblyPath) { return AppDomain.CurrentDomain.Load(assemblyPath); } public override object InitializeLifetimeService() { return null; } public void SetMessageQueue( HostServiceMessageQueue hostMsgQueue) { mHostMessageQueue = hostMsgQueue; AppDomain.CurrentDomain.SetData(ServiceDomainTags.TAG_PLUGINMESSAGEQUEUE, mMessageQueue); AppDomain.CurrentDomain.SetData(ServiceDomainTags.TAG_HOSTMESSAGEQUEUE, hostMsgQueue); } String assemblyFolder; public bool LoadAssembly(string assemblyFile) { assemblyFolder = Path.GetDirectoryName(assemblyFile); AppDomain.CurrentDomain.SetData(ServiceDomainTags.TAG_ASSEMBLYLOCATION, assemblyFile); AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; byte[] bytes = File.ReadAllBytes(assemblyFile); Assembly assembly = Assembly.Load(bytes); // Assembly assembly = Assembly.LoadFrom(assemblyFile); MethodInfo main = assembly.EntryPoint; Task.Run(() => { // main.Invoke(null, new object[] { null}); string[] str = { "runasplugin" };/// Fill str object[] obj = new object[1]; obj[0] = str; string[] myArray = { "runasplugin" }; try { // AppDomain.CurrentDomain.ExecuteAssembly(assemblyFile, str); main.Invoke(null, obj); } catch(Exception ex) { //ServiceManager.getInstance().UnLoadModule(moduleName); // arg = true; mHostMessageQueue.AddMessage(new HostServiceMessage { Type="plugin_crash", Source=assemblyFolder ,Content = ex.Message }); } }); int nCount = 0; while(true) { Boolean? isReady = (Boolean?)AppDomain.CurrentDomain.GetData(ServiceDomainTags.TAG_READY); if(isReady != null&& isReady.Value) { break; } Thread.Sleep(1000); if (++nCount >= EXECUTION_TIMEOUT) return false; } return true; } private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) { Console.WriteLine("CurrentDomain_AssemblyResolve:" + args.RequestingAssembly.CodeBase); string[] Parts = args.Name.Split(','); string strFilePath = Path.Combine(assemblyFolder, Parts[0].Trim() + ".dll"); if(File.Exists(strFilePath)) { return System.Reflection.Assembly.LoadFrom(strFilePath); } return null; } public void Start() { mMessageQueue.AddMessage(new HostServiceMessage { Type = HostMessageType.STARTSERVICE }); } public void Stop() { mMessageQueue.AddMessage(new HostServiceMessage { Type = HostMessageType.STOPSERVICE }); int nCount = 0; while (true) { Boolean? isExit = (Boolean?)AppDomain.CurrentDomain.GetData(ServiceDomainTags.TAG_EXIT); if (isExit != null && isExit.Value) { break; } Thread.Sleep(1000); if (++nCount >= EXECUTION_TIMEOUT) return; } } public String GetName() { return (String)AppDomain.CurrentDomain.GetData(ServiceDomainTags.TAG_SERVICENANME); } public String GetVersion() { return (String)AppDomain.CurrentDomain.GetData(ServiceDomainTags.TAG_SERVICEVERSION); } public String GetId() { return (String)AppDomain.CurrentDomain.GetData(ServiceDomainTags.TAG_SERVICEID); } }
前面提过,为了保证数据隔离,servicerunner的实例需要在一个新建的程序域创建
HostServiceMessageQueue hostMessageQueue = new HostServiceMessageQueue();AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += CurrentDomain_ReflectionOnlyAssemblyResolve; AppDomainSetup setup = new AppDomainSetup(); setup = AppDomain.CurrentDomain.SetupInformation; setup.ApplicationName = "ApplicationLoader"; setup.ShadowCopyFiles = "false"; setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; setup.ConfigurationFile = entryLocation + ".config"; this.appDomain = AppDomain.CreateDomain("ApplicationLoaderDomain", null, setup); this.remoteLoader = (ServiceRunner)this.appDomain.CreateInstanceAndUnwrap( typeof(ServiceRunner).Assembly.FullName, typeof(ServiceRunner).FullName); this.remoteLoader.SetMessageQueue(hostMessageQueue);
通过messagequeue我们可以实现对子服务的操作:升级,日志,启动,停止
public class HostMessageType { public static readonly String LOG = "log"; public static readonly String UPDATE = "update"; public static readonly string STARTSERVICE = "start"; public static readonly string STOPSERVICE = "stop"; }
以下是子服务需要派生的完整的Hosrservicebase类的实现
// base class each plugin must inherit public abstract class HostServiceBase { String mAssemblyPath; // plugin assembly location path HostServiceMessageQueue mMessagequeue = null; // messsqge queue contains message from hostService HostServiceMessageQueue mHostMessagequeue = null; // messagque in hostservice, plugin can put message to hostservice in this queue protected abstract void OnStart(String assemblyLocation); // trigger plugin logic to start protected abstract void OnStop(); // trigger plugin logic to stop protected abstract String GetServiceName();// plugin friendly name protected abstract String GetVersion(); // can be used to store verison of assembly protected String GetServiceID() { return "E85DE0B7-524F-46C9-9889-9243FB53258A"; } // this should be a unique GUID for the plugin - a different one may be used for each version of the plugin. bool _stop = false; protected void DoStart(String assemblyLocation) { Log("do start"); _stop = false; OnStart(assemblyLocation); } protected void DoStop() { Log("do stop"); _stop = true; OnStop(); } // log to hostservice domain protected void Log(String strLog) { Console.WriteLine(strLog); if (mHostMessagequeue != null) { HostServiceMessage evt = new HostServiceMessage(); evt.Source = GetServiceID(); evt.Type = HostMessageType.LOG; evt.Content = @"(" + GetServiceName()+@") "+strLog; mHostMessagequeue.AddMessage(evt); } } public String GetAssemblyFolderPath() { return mAssemblyPath; } // plugin working thread,need to executed in plugin main entry point public void Execute(bool bUseServiceBus) { Log("Start to execute"); if (bUseServiceBus) { AppDomain.CurrentDomain.SetData(ServiceDomainTags.TAG_SERVICENANME,GetServiceName()); AppDomain.CurrentDomain.SetData(ServiceDomainTags.TAG_SERVICEID, GetServiceID()); AppDomain.CurrentDomain.SetData(ServiceDomainTags.TAG_SERVICEVERSION, GetVersion()); AppDomain.CurrentDomain.SetData(ServiceDomainTags.TAG_READY,true); mMessagequeue = (HostServiceMessageQueue) AppDomain.CurrentDomain.GetData(ServiceDomainTags.TAG_PLUGINMESSAGEQUEUE); mHostMessagequeue = (HostServiceMessageQueue)AppDomain.CurrentDomain.GetData(ServiceDomainTags.TAG_HOSTMESSAGEQUEUE); mAssemblyPath =(String) AppDomain.CurrentDomain.GetData(ServiceDomainTags.TAG_ASSEMBLYLOCATION); if (mMessagequeue == null || mHostMessagequeue == null) return; while(true) { HostServiceMessage message = mMessagequeue.TakeMessage(); // if(message.Target.CompareTo(GetServiceID()) == 0) { if(message.Type.CompareTo(HostMessageType.STOPSERVICE) == 0) { DoStop(); break; } if (message.Type.CompareTo(HostMessageType.STARTSERVICE) == 0) { DoStart(mAssemblyPath); } } } } else { DoStart(Assembly.GetExecutingAssembly().Location); Console.WriteLine("Press any key to stop"); Console.ReadLine(); DoStop(); } AppDomain.CurrentDomain.SetData(ServiceDomainTags.TAG_EXIT, true); } }