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

SelfServe:一个自托管、自安装的 Windows 服务

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2020年1月15日

MIT

6分钟阅读

viewsIcon

20533

downloadIcon

291

添加从命令行运行服务到控制台模式以及控制或安装服务的功能

引言

我不喜欢安装程序。我不喜欢必须安装服务才能使用它们,因此它们无论做什么都需要管理员权限。这就是为什么我编写的服务是自托管和自安装的。我将向您展示它的样子,以及这对您的代码及其用户意味着什么。

概念化这个混乱的局面

Windows 服务是微软对 POSIX 守护进程的一种功能更强大的解决方案。它们允许您运行一个执行某些工作的后台进程,该进程通常在操作系统会话的生命周期内存在。为了使用它们,您不能仅仅从控制台运行它们,而必须首先安装服务,然后通过控制面板启动它(假设它没有设置为自动启动,尽管几乎所有服务都是如此)。安装通常需要管理员权限,因为它会影响整个系统。

SelfServe 将以两种模式之一运行服务:一种是“交互模式”,在此模式下,服务可以按每个用户会话的基础启动,在当前命令提示符的上下文中运行,并且直到停止才阻塞;另一种是“非交互模式”(安装后)。在后一种情况下,它像普通的 Windows 服务一样运行,是一个后台进程,不会阻塞命令窗口。

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

尽管如此,使用它很简单

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

   /start      Starts the service, if it's not already running. When not installed, 
               this runs in console mode.
   /stop       Stops the service, if it's running. This will stop the installed service, 
               or kill the console mode service process.
   /install    Installs the service, 
               if not installed so that it may run in Windows service mode.
   /uninstall  Uninstalls the service, if installed, 
               so that it will not run in Windows service mode.
   /status     Reports if the service is installed and/or running.
  • /start - 当安装为 Windows 服务时,它将启动该服务。当未安装时,它将在控制台模式下启动该服务,前提是它尚未运行。
  • /stop - 当安装为 Windows 服务时,它将停止该服务。当未安装时,它将停止该用户已运行的任何实例。
  • /install - 将 SelfServe 安装为 Windows 服务。运行此命令需要管理员权限。
  • /uninstall - 卸载 SelfServe 作为 Windows 服务。运行此命令需要管理员权限。

这掩盖了大量复杂性,使其易于使用。我不得不付出很多努力才使其正常工作。

编写这个混乱的程序

由于其功能,此项目不能作为库分发。相反,要使用此代码库创建自己的服务,只需将 Program.csService.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 服务启动

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

以 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
    // so we reflect
    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() 后,我们只需无限循环等待进程终止。这可以使服务保持运行状态,直到进程被终止(通常是通过另一个使用 /stop 运行的 SelfServe 实例)。

停止服务

停止服务有两种方式,取决于它是否为 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.");
        }
    }
}

请注意,我在这里创建了一个互斥体。虽然我并没有避免所有可能的竞态条件,但这试图通过在服务启动时“锁定”应用程序(或在安装时安装)来避免其中一个。

在您自己的项目中启用

请记住,将 Program.csService.cs 复制到您自己的项目中。添加对 System.ServiceProcess 的引用,并通过“属性”面板在 Service 组件上设置 ServiceName。然后只需将您的服务代码添加到 Service.cs 中。

历史

  • 2020年1月14日 - 首次提交
© . All rights reserved.