在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.");
}
}
}
在这里,创建了一个互斥体。虽然没有避免每一个可能的竞争条件,但这试图通过“锁定”应用程序来避免一个竞争条件,即在启动时安装或在安装时启动。