Windows 服务自动更新插件框架






4.99/5 (27投票s)
使用插件系统自动更新 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 中进行查看。
插件管理器
插件管理器包含两个主要类。PluginHost
和 Controller
。所有这些都使用 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版发布