65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows 服务自动更新插件框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (27投票s)

2013年12月24日

CPOL

6分钟阅读

viewsIcon

92878

downloadIcon

2946

使用插件系统自动更新 Windows 服务的 [功能], 无需重新安装服务。

引言

Windows 服务是后台运行的长期进程,但它们不需要更新,并不意味着它们不需要更新。服务的一个问题是,当代码需要更新时,它们需要管理员权限才能安装和重新安装。本文介绍了一个框架,该框架允许您在需要时更新 Windows 服务中的代码,而无需管理员干预。提供的代码有效,并且更集成的版本已投入生产 - 本文在此提供总体概述。根据您的具体要求,可以进行许多调整和不同的方法来实现细节。希望这能为您的特定解决方案提供一个良好的开端。我鼓励您下载代码,如果您有任何改进,请留下评论并发送给我,以便大家都能受益。目前提出的方法非常粗糙,留下了很多内部适应性由开发人员自行完成。在将来的版本中,我将把代码打包成一个更健壮的自管理框架,该框架可以作为软件包安装,其中包括我目前正在开发的其他一些有趣的东西!

背景

如果您在少数几个站点上运行 Windows 服务并需要更新它,这不成问题……只需远程连接,使用命令行,然后即可完成。但是,当您拥有庞大的已安装用户群时,情况开始变得有些棘手。这里讨论的框架设计旨在作为一个大型自管理/远程更新生态系统的基础。这里介绍了主要概念,并为您的实现提供了建议。

简而言之

框架背后的主要概念是将所有工作从服务本身中移除,并仅将其用作加载/卸载执行任何必需工作的插件程序集的“外壳”。这是通过将程序集加载到一个或多个应用程序域来实现的,这些应用程序域至关重要的是独立于主服务主机应用程序域。之所以需要单独的域,是因为虽然您可以轻松地将插件加载到当前/主应用程序域中,但您只能一次性卸载整个域,而不能更具体。如果我们将在与核心服务应用程序相同的域中加载插件,那么默认情况下,卸载插件也会卸载主应用程序。在此框架实现中,服务主机只需要知道两件事 - 何时加载/卸载插件以及如何加载/卸载插件。所有其他内容都由插件主机控制器和插件本身处理。

操作 (Operation)

框架运行如下

~ 设置 ~

服务可以做两件事(1)创建一个插件控制器并使用 MarshalByRef 对象将其保持在“安全距离”之外,(2)接收插件控制器发送给它的事件消息。

~ 管理 ~

插件控制器按需创建 1..n 个应用程序域。在此演示中,我创建了一个名为“command”的域和一个名为“plugins”的域。概念是“command”域可用于检查 Web 服务以获取插件的更新版本,并以此启动“refresh / reload”例程,而“plugins”域则执行一些工作进程。命令插件通常包含一个计划程序对象,该对象会按特定时间间隔触发操作。

~ 消息传递 ~

框架由从插件流向控制器,再流向上层主机服务程序的消息控制。消息可以是简单的日志和通知消息,也可以是告诉控制器或服务触发特定操作的操作消息。触发操作可以是诸如“检查服务器上的新版本”、“连接到主服务器”、“加载/卸载特定应用程序域”之类的命令。由于目标是将所有工作和逻辑都移出服务,因此请注意将工作分离到离散的插件包中。并非所有插件都需要一直加载并消耗资源。通过使用不同的应用程序域,您可以使用主调度程序插件按需进行加载/卸载。

插件定义

对于任何插件系统,一个重要的构建块是插件控制器可以管理的已知接口定义。为了启动,我创建了一个包含我所需的最少功能的接口。这包括用于标志一个正在运行的进程需要停止,并在其完成进程运行时发出自卸载事件的方法。

    // Interface each plugin must implement
    public interface IPlugin
    {
        string PluginID();               // this should be a unique GUID for the plugin - a different 
                                         // one may be used for each version of the plugin.
        bool TerminateRequestReceived(); // internal flag if self-terminate request has been received
        string GetName();                // assembly friendly name
        string GetVersion();             // can be used to store version of assembly
        bool Start();                    // trigger assembly to start
        bool Stop();                     // trigger assembly to stop
        void LogError(string Message, EventLogEntryType LogType); // failsafe - logs to 
                                                                  // eventlog on error
        string RunProcess();             // main process that gets called
        void Call_Die();                 // process that gets called to kill the current plugin
        void ProcessEnded();             // gets called when main process ends, 
                                         // i.e.,: web-scrape complete, etc...

        // custom event handler to be implemented, event arguments defined in child class
        event EventHandler<plugineventargs> CallbackEvent;
        PluginStatus GetStatus(); // current plugin status (running, stopped, processing...)
    }

当我们通过远程边界发送消息时,我们需要序列化消息。对于此实现,我选择创建一个自定义 EventArgs 类与我的事件消息一起发送。

    // event arguments defined, usage: ResultMessage is for any error trapping messages, 
    // result bool is fail/success
    // "MessageType" used to tell plugin parent if it needs to record a message or take an action, etc.
    [Serializable]
    public class PluginEventArgs : EventArgs
    {
        public PluginEventMessageType MessageType;
        public string ResultMessage;
        public bool ResultValue;
        public string MessageID;
        public string executingDomain;
        public string pluginName;
        public string pluginID;
        public PluginEventAction EventAction;
        public CallbackEventType CallbackType; 
 
        public PluginEventArgs(PluginEventMessageType messageType = 
        PluginEventMessageType.Message, string resultMessage = "",PluginEventAction eventAction = 
        (new PluginEventAction()), bool resultValue = true)
        {
            // default empty values allows us to send back default event response
            this.MessageType = messageType;     // define message type that is bring sent
            this.ResultMessage = resultMessage; // used to send any string messages
            this.ResultValue = resultValue;
            this.EventAction = eventAction;     // if the event type = "Action" 
                                                // then this carries the action to take
            this.executingDomain = AppDomain.CurrentDomain.FriendlyName;
            this.pluginName = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;
            //this.pluginID = ((IPlugin)System.Reflection.Assembly.GetExecutingAssembly()).PluginID();
        }
    }

正如您所见,有许多支持类型和类 - 我不想将整个代码复制到文章中,所以如果您想查看细节,请下载附加的代码并在 Visual Studio 中进行查看。

插件管理器

插件管理器包含两个主要类。PluginHostController。所有这些都使用 MarshalByRefObject 包装为远程对象。

插件主机

主机将控制器和插件与主应用程序保持“安全距离”。它定义并设置不同的应用程序域,然后调用控制器来加载和管理插件本身。

    public class PluginHost : MarshalByRefObject
    {
        private const string DOMAIN_NAME_COMMAND = "DOM_COMMAND";
        private const string DOMAIN_NAME_PLUGINS = "DOM_PLUGINS";

        private AppDomain domainCommand;
        private AppDomain domainPlugins;
        
        private PluginController controller_command;
        private PluginController controller_plugin;

        public event EventHandler<plugineventargs> PluginCallback;
        ...

加载到域中

        public void LoadDomain(PluginAssemblyType controllerToLoad)
        {
            init();
            switch (controllerToLoad)
            {
                case PluginAssemblyType.Command:
                    {
                        controller_command = (PluginController)domainCommand.CreateInstanceAndUnwrap
                        ((typeof(PluginController)).Assembly.FullName, 
                         (typeof(PluginController)).FullName);
                        controller_command.Callback += Plugins_Callback;
                        controller_command.LoadPlugin(PluginAssemblyType.Command);
                        return;
                    }
                case PluginAssemblyType.Plugin:
                    {
                        controller_plugin = (PluginController)domainPlugins.CreateInstanceAndUnwrap
                        ((typeof(PluginController)).Assembly.FullName, 
                        (typeof(PluginController)).FullName);
                        controller_plugin.Callback += Plugins_Callback;
                        controller_plugin.LoadPlugin(PluginAssemblyType.Plugin);
                        return;
                    }
            }
        }
...

插件控制器

插件控制器最接近插件本身。它是消息流的第一站,负责控制插件之间的消息流,以及从插件流回服务应用程序程序。

        void OnCallback(PluginEventArgs e)
        {
            // raise own callback to be hooked by service/application
            // pass through callback messages received if relevant
            if (e.MessageType == PluginEventMessageType.Action)
            {
           ....
                else if (e.EventAction.ActionToTake == PluginActionType.Unload) // since the plugin 
                // manager manages plugins, we intercept this type of message and dont pass it on
                {
           ....
            else
            {
                if (Callback != null) // should ONLY happen is not type action and only message
                {
                    Callback(this, e);
                }
            }

插件

在这个演示示例中,插件被设计得非常简单。除一个外,所有插件的代码都相同。它们有一个计时器,在 onInterval 时,它们会将消息打印到控制台。如果它们收到关闭消息,它们会立即关闭,除非它们正在执行某个进程,在这种情况下,它们将完成该进程,然后发出已准备好卸载的信号。

        public bool Stop()
        {
            if (_Status == PluginStatus.Running) // process running - 
                        // cannot die yet, instead, flag to die at next opportunity
            {
                _terminateRequestReceived = true;
                DoCallback(new PluginEventArgs(PluginEventMessageType.Message, 
                "Stop called but process is running from: " + _pluginName)); 
            }
            else
            {
                if (counter != null)
                {
                    counter.Stop();
                }
                _terminateRequestReceived = true;
                DoCallback(new PluginEventArgs(PluginEventMessageType.Message, 
                "Stop called from: " + _pluginName));
                Call_Die();
            }
            
            return true;
        }

...

        // OnTimer event, process start raised, sleep to simulate doing some work, 
        // then process end raised
        public void OnCounterElapsed(Object sender, EventArgs e)
        {
            _Status = PluginStatus.Processing;
            DoCallback(new PluginEventArgs(PluginEventMessageType.Message, 
            "Counter elapsed from: " + _pluginName));
            if (_terminateRequestReceived)
            {
                counter.Stop();
                DoCallback(new PluginEventArgs(PluginEventMessageType.Message, 
                "Acting on terminate signal: " + _pluginName));
                _Status = PluginStatus.Stopped;
                Call_Die();
            }
            else
            {
                _Status = PluginStatus.Running; // nb: in normal plugin, 
                // this gets set after all processes complete - may be after scrapes etc.
            }
        }

“命令/控制”插件模拟了请求服务自身更新(嘿,终于,我们来这里的目的!)....

        // OnTimer event, process start raised, sleep to simulate doing some work, 
        // then process end raised
        public void OnCounterElapsed(Object sender, EventArgs e)
        {
            _Status = PluginStatus.Processing;
            DoCallback(new PluginEventArgs(PluginEventMessageType.Message, 
                                           "Counter elapsed from: " + _pluginName));
            if (_terminateRequestReceived)
            {
                DoCallback(new PluginEventArgs(PluginEventMessageType.Message, 
                "Counter elapsed, terminate received, stopping process...  from: " + _pluginName));
            }

            // TEST FOR DIE...
            DoCallback(new PluginEventArgs(PluginEventMessageType.Message, 
            "*** Sending UPDATE SERVICE WITH INSTALLER COMMAND ***"));
            PluginEventAction actionCommand = new PluginEventAction();
            actionCommand.ActionToTake = PluginActionType.TerminateAndUnloadPlugins; // TEST !!!! ... 
            //this should ONLY be used to signal the HOST/CONTROLLER to flag a DIE....
            DoCallback(new PluginEventArgs(PluginEventMessageType.Action, null, actionCommand)); 
            DoCallback(new PluginEventArgs(PluginEventMessageType.Message, 
            "*** Sending UPDATE SERVICE WITH INSTALLER COMMAND - COMPLETE ***"));
            Call_Die();
            // end test
        }

一段关键的“陷阱”代码重写了 MarshalByRef 的“InitializeLifetimeService”方法。默认情况下,远程对象会在一小段时间后失效。通过重写此方法,您可以确保您的对象一直处于活动状态,直到您希望为止。

    public override object InitializeLifetimeService()
    {
        return null;
    }

服务程序

当我们启动服务时,我们会挂钩插件管理器事件回调。

        public void Start()
        {
            if (pluginHost == null)
            { 
                pluginHost = new PluginHost();
                pluginHost.PluginCallback += Plugins_Callback;
                pluginHost.LoadAllDomains();
                pluginHost.StartAllPlugins();
            }
        }

当卸载事件冒泡时,我们可以启动一个 MSI 安装程序,该安装程序以静默模式运行,并用它来更新插件本身。MSI 安装程序仅仅是整洁地打包的一种方式。目标是以静默模式运行 msi,因此不需要用户交互。您也可以使用 nuget 等,我将在后续迭代中对此进行研究。

        private void Plugins_Callback(object source, PluginContract.PluginEventArgs e)
        {
            if (e.MessageType == PluginEventMessageType.Message)
            {
                EventLogger.LogEvent(e.ResultMessage, EventLogEntryType.Information);
                Console.WriteLine(e.executingDomain + " - " + 
                e.pluginName + " - " + e.ResultMessage); // for debug    
            }
            else if (e.MessageType == PluginEventMessageType.Action) {
                if (e.EventAction.ActionToTake == PluginActionType.UpdateWithInstaller)
                {
                    Console.WriteLine("****  DIE DIE DIE!!!! 
                    ... all plugins should be DEAD and UNLOADED at this stage ****");
                    EventLogger.LogEvent("Update with installer event received", 
                    EventLogEntryType.Information);
                    // Plugin manager takes care of shutting things down 
                    // before calling update so we are safe to proceed...
                    if (UseInstallerVersion == 1)
                    {
                        EventLogger.LogEvent("Using installer 1", EventLogEntryType.Information);
                        UseInstallerVersion = 2;
                        // run installer1 in silent mode - it should replace files, 
                        // and tell service to re-start
                    }
                    else if (UseInstallerVersion == 2)
                    {
                        EventLogger.LogEvent("Using installer 2", EventLogEntryType.Information);
                        // run installer2 in silent mode - it should replace files, 
                        // and tell service to re-start
                        UseInstallerVersion = 1;
                    }
                }
            }
        }

恭喜,您现在拥有了一个自更新的 Windows 服务,一旦安装,就可以在几乎无需干预的情况下进行远程管理。

历史

  • 2013年12月22日 - 第1版发布
© . All rights reserved.