自我安装与托管服务的技术实现

在Windows操作系统中,服务是一种在后台运行的程序,通常用于执行特定的任务。这些服务需要管理员权限才能安装和运行,因为它们涉及到系统级别的操作。然而,这种权限要求可能会给用户带来不便。为了解决这个问题,开发了一种自安装和自托管的服务,它允许用户在不需要管理员权限的情况下运行服务。本文将详细介绍这种服务的工作原理以及如何实现它。

概念化服务

Windows服务是微软对POSIX守护进程的一种实现,它允许用户运行一个在操作系统会话期间持续存在的后台进程。与简单地从控制台运行程序不同,用户必须首先安装服务,然后通过控制面板启动它(假设它没有设置为自动启动,尽管几乎所有服务都是自动启动的)。安装服务通常需要管理员权限,因为它涉及到系统级别的操作。

自服务可以在两种不同的模式下运行:一种是“交互模式”,在这种模式下,服务可以根据每个用户会话启动,运行在当前命令提示符的上下文中,并在停止之前阻塞;另一种是非交互模式,即安装后运行。在后一种情况下,它像普通的Windows服务一样运行,是一个不会阻塞命令窗口的后台进程。

自服务强制执行单例语义,即一次只能运行一个服务实例;如果后台Windows服务正在运行,则不能启动交互模式服务;如果交互模式服务正在运行,则不能启动服务。需要注意的是,在多用户的情况下,仍然可以运行多个服务实例。除了托管服务外,自服务还用作控制器应用程序,用于启动、停止、安装、卸载和检查服务的状态。

使用自服务非常简单:

Usage: SelfServe.exe /start | /stop | /install | /uninstall | /status

这些命令分别用于启动服务、停止服务、安装服务、卸载服务和报告服务的状态。

编码实现

由于自服务的功能,它不能作为库分发。相反,要使用这个代码库创建自己的服务,只需将Program.cs和Service.cs复制到自己的服务项目中,然后确保在服务组件上设置属性,特别是ServiceName属性(不要与Name属性混淆)。

为了获取服务的属性——实际上是ServiceName属性,创建了一个服务类的临时实例,然后读取它的属性。这样,开发者只需要在一个地方设置属性——在设计器中的服务本身。否则,将不得不创建自己的机制来定义服务名称。如果要启动服务,简单地回收那个实例。否则,它在进程退出时会被丢弃,而不会运行。

使用命名互斥体来实现这一点。每次服务启动时,无论是在Windows服务模式还是在控制台模式下,都会创建一个这样的互斥体,使用服务名称:

bool createdNew = true; using (var mutex = new Mutex(true, svctmp.ServiceName, out createdNew)) { if (createdNew) { mutex.WaitOne(); // run code here... } else { throw new ApplicationException("The service " + svctmp.ServiceName + " is already running."); } }

这确保了一次只能启动一个实例。

用来确定应用程序是从头命令行还是作为Windows服务运行的机制非常简单。只需检查Environment.UserInteractive属性。如果为true,则应用程序是从命令行运行的。否则,它是作为Windows服务运行的。

在Windows服务模式下,像正常一样启动服务:

ServiceBase[] ServicesToRun; ServicesToRun = new ServiceBase[] { svctmp }; ServiceBase.Run(ServicesToRun);

在控制台模式下运行服务稍微复杂一些,因为需要自己托管服务类:

var type = svc.GetType(); var thread = new Thread(() => { // HACK: In order to run this service outside of a service context, // we must call the OnStart() protected method directly var args = new string[0]; type.InvokeMember("OnStart", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, svc, new object[] { args }); while (true) { Thread.Sleep(0); } }); thread.Start(); thread.Join(); // probably never run, but let's be sure to call it if it does type.InvokeMember("OnStop", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, svc, new object[0]);

在这里,可以看到事情变得有点复杂。主要需要注意的是使用了反射!不得不这样做,因为不能调用service.Start()来启动服务,因为它没有作为Windows服务安装。相反,只想直接调用OnStart(),以便运行服务的初始化代码,但该方法是受保护的。OnStop()也是如此。此外,还在另一个线程上执行所有这些操作。这并不绝对必要,但想给服务一个自己的线程上下文,而不是主应用程序线程。在那个线程中,在调用OnStart()之后,简单地无限循环等待进程死亡。这使得服务保持活动状态,直到进程被杀死(通常是通过另一个SelfServe实例运行/stop命令)。

停止服务的方式取决于它是Windows服务还是其他类型的服务。如果它是Windows服务,服务将被停止。否则,将枚举每个用户进程,并杀死与此进程名称匹配的任何进程(除了此进程本身)。

static void _StopService(string name, bool isInstalled) { if (isInstalled) { ServiceInstaller.StopService(name); } else { var id = Process.GetCurrentProcess().Id; var procs = Process.GetProcesses(); for (var i = 0; i < procs.Length; ++i) { var proc = procs[i]; var f = proc.ProcessName; if (id != proc.Id && 0 == string.Compare(Path.GetFileNameWithoutExtension(_File), f)) { try { proc.Kill(); if (!proc.HasExited) proc.WaitForExit(); } catch { } } } } _PrintStatus(name); }

初步尝试是直接使用ServiceInstaller来驱动InstallUtil.exe,但这种方法有几个问题。首先,它不起作用,因为它需要一些未记录的状态,或者至少没有找到。尝试调用Install()时,无论是带参数还是不带参数,都会出错。其次,有些系统可能根本没有InstallUtil.exe,这种情况下也会失败。

不幸的是,不得不实现对advapi32.dll的本地调用来安装或卸载服务。Stack Overflow上有一个非常好的ServiceInstaller类的实现——(由Lars A. Brekken提供——原始作者未知),所以就直接使用了。还使用这个类来启动和停止Windows服务。使用它非常简单,如下所示:

static void _InstallService(string name) { var createdNew = true; using (var mutex = new Mutex(true, name, out createdNew)) { if (createdNew) { mutex.WaitOne(); ServiceInstaller.Install(name, name, _FilePath); Console.Error.WriteLine("Service " + name + " installed"); } else { throw new ApplicationException("Service " + name + " is currently running."); } } }

在这里,创建了一个互斥体。虽然没有避免每一个可能的竞争条件,但这试图通过“锁定”应用程序来避免一个竞争条件,即在启动时安装或在安装时启动。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485