在服务器上安装多个相同的Windows服务实例

105

我们已经开发了一个Windows服务,以向客户端应用程序提供数据,并且一切都非常顺利。客户提出了一个有趣的配置请求,需要在同一台服务器上运行两个该服务的实例,并配置为分别指向不同的数据库。

到目前为止,我还没有成功实现这个需求,希望我的StackOverflow网友们能够给一些提示。

当前设置:

我已经设置了包含Windows服务的项目,我们从现在起将其称为AppService,以及ProjectInstaller.cs文件,该文件处理自定义安装步骤,根据App.config中的关键字设置服务名称,如下所示:

this.serviceInstaller1.ServiceName = Util.ServiceName;
this.serviceInstaller1.DisplayName = Util.ServiceName;
this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.LocalSystem;
在这种情况下,Util只是一个静态类,从配置文件中加载服务名称。
从这里开始,我尝试了两种不同的方法来安装两个服务,但都以相同的方式失败。
第一种方法是简单地安装第一个服务副本,复制已安装的目录并将其重命名,然后在修改应用程序配置以更改所需服务名称后运行以下命令:
InstallUtil.exe /i AppService.exe

当我尝试使用第一个安装程序无法成功后,我尝试创建了第二个安装程序,编辑了配置文件并构建了第二个安装程序。当我运行安装程序时,它可以正常工作,但服务未显示在services.msc中,所以我针对第二个已安装的代码库运行了先前的命令。

两次都收到了以下来自InstallUtil的输出(仅包含相关部分):

 

正在运行事务安装。

    

开始安装过程。

    

正在安装服务App Service Two...   服务App Service Two已成功安装。   在应用程序日志中创建EventLog源App Service Two...

    

在安装阶段发生异常。   System.NullReferenceException:对象引用未设置为对象的实例。

    

还原应用程序日志以恢复源App Service Two的之前状态。   正在从系统中删除服务App Service Two...   服务App Service Two已成功从系统中删除。

    

回滚阶段已成功完成。

    

事务安装已完成。   安装失败,并执行了回滚操作。

很抱歉我的帖子有点长,我想确保提供了足够的相关信息。目前让我感到困惑的是,它表示服务的安装已成功完成,只有在尝试创建EventLog源后似乎才会引发NullReferenceException。因此,如果有人知道我做错了什么或有更好的方法,将不胜感激。

10个回答

85

6
我发现这个页面很有用:http://journalofasoftwaredev.wordpress.com/2008/07/16/multiple-instances-of-same-windows-service/。你可以在安装程序中插入代码,在运行installutil时获取所需的服务名称。 - Vivian River
9
博客链接已更改为:http://journalofasoftwaredev.wordpress.com/2008/07/ - STLDev

23
  sc create [servicename] binpath= [path to your exe]

这个解决方案对我有用。


6
只是想指出,[path to your exe] 必须是完整路径,并且不要忘记在 binpath= 后添加空格。 - mkb
3
这确实允许一个服务被多次安装。然而,所有由服务安装程序提供的信息,例如描述、登录类型等都将被忽略。 - Noel Widmer

20
你可以通过以下方式同时运行多个版本的同一服务:
1)将服务可执行文件和配置复制到其自己的文件夹中。
2)从.NET Framework文件夹中将Install.exe复制到服务可执行文件夹中。
3)在服务可执行文件夹中创建一个名为Install.exe.config的配置文件,并填写以下内容(唯一的服务名称):
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="ServiceName" value="The Service Name"/>
    <add key="DisplayName" value="The Service Display Name"/>
  </appSettings>
</configuration>

4)创建一个批处理文件,内容如下,用于安装服务:

REM Install
InstallUtil.exe YourService.exe
pause

5) 同时,创建一个卸载批处理文件

REM Uninstall
InstallUtil.exe -u YourService.exe
pause

编辑:

不确定是否有遗漏,这是 ServiceInstaller 类(根据需要进行调整):

using System.Configuration;

namespace Made4Print
{
    partial class ServiceInstaller
    {
        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;
        private System.ServiceProcess.ServiceInstaller FileProcessingServiceInstaller;
        private System.ServiceProcess.ServiceProcessInstaller FileProcessingServiceProcessInstaller;

        /// <summary> 
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Component Designer generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.FileProcessingServiceInstaller = new System.ServiceProcess.ServiceInstaller();
            this.FileProcessingServiceProcessInstaller = new System.ServiceProcess.ServiceProcessInstaller();
            // 
            // FileProcessingServiceInstaller
            // 
            this.FileProcessingServiceInstaller.ServiceName = ServiceName;
            this.FileProcessingServiceInstaller.DisplayName = DisplayName;
            // 
            // FileProcessingServiceProcessInstaller
            // 
            this.FileProcessingServiceProcessInstaller.Account = System.ServiceProcess.ServiceAccount.LocalSystem;
            this.FileProcessingServiceProcessInstaller.Password = null;
            this.FileProcessingServiceProcessInstaller.Username = null;
            // 
            // ServiceInstaller
            // 
            this.Installers.AddRange(new System.Configuration.Install.Installer[] { this.FileProcessingServiceInstaller, this.FileProcessingServiceProcessInstaller });
        }

        #endregion

        private string ServiceName
        {
            get
            {
                return (ConfigurationManager.AppSettings["ServiceName"] == null ? "Made4PrintFileProcessingService" : ConfigurationManager.AppSettings["ServiceName"].ToString());
            }
        }

        private string DisplayName
        {
            get
            {
                return (ConfigurationManager.AppSettings["DisplayName"] == null ? "Made4Print File Processing Service" : ConfigurationManager.AppSettings["DisplayName"].ToString());
            }
        }
    }
}

我有一个模板一直在用,所以可能会错过一些东西。你的ServiceInstaller类是什么样子的?我会发布一个我使用的工作副本,让我知道这是否有帮助? - Mark Redman
2
非常感谢您的帮助。我认为安装配置文件应该被命名为InstallUtil.exe.confg而不是Install.exe.config,以供InstallUtil.exe使用。 - NullReference
一种非常好的方法,完全可行。前提是你知道要复制哪个InstallUtil.exe到你的安装文件夹中(我个人安装了大量的框架版本,这被64位副本所恶化)。如果由Helpdesk团队进行安装,这将使解释变得非常困难。但对于开发人员主导的安装来说,这非常优雅。 - timmi4sa
我不得不将这段代码添加到项目安装程序类中,但它没有出现任何问题。 - fizch
ConfigurationManager.AppSettings["ServiceName"] 对我总是返回 null,即使我在 app.config 中设置了它们。有什么想法吗?如果我调试项目,它会读取它,但如果我从 serviceutil 运行它,它总是将服务名称设置为默认值(硬编码的值)。 - Teoman shipahi
显示剩余10条评论

15

通过使用installutil命令行参数,您可以另一种快速指定ServiceNameDisplayName自定义值的方法。

  1. 在您的ProjectInstaller类中覆盖虚拟方法Install(IDictionary stateSaver)Uninstall(IDictionary savedState)

public override void Install(System.Collections.IDictionary stateSaver)
{
    GetCustomServiceName();
    base.Install(stateSaver);
}

public override void Uninstall(System.Collections.IDictionary savedState)
{
    GetCustomServiceName();
    base.Uninstall(savedState);
}

//Retrieve custom service name from installutil command line parameters
private void GetCustomServiceName()
{
    string customServiceName = Context.Parameters["servicename"];
    if (!string.IsNullOrEmpty(customServiceName))
    {
        serviceInstaller1.ServiceName = customServiceName;
        serviceInstaller1.DisplayName = customServiceName;
    }
}
  • 构建您的项目
  • 使用installutil安装服务,并使用/servicename参数添加您的自定义名称:

  • installutil.exe /servicename="CustomServiceName" "c:\pathToService\SrvcExecutable.exe"
    
    请注意,如果您在命令行中没有指定/servicename,则服务将使用ProjectInstaller属性/配置中指定的ServiceName和DisplayName值进行安装。

    2
    太棒了!谢谢你,这正是所需的,而且简明扼要。 - Iofacture

    11

    我知道这是一个老问题,但我在使用InstallUtil.exe时,使用/servicename选项取得了成功。不过我没有在内置帮助中看到它被列出。

    InstallUtil.exe /servicename="My Service" MyService.exe
    

    我不确定我最初在哪里读到这个,但自那以后我就再也没有看到过了。你的情况可能会有所不同。


    3
    返回此错误信息:在安装阶段发生了异常。 System.ComponentModel.Win32Exception: 指定的服务已经存在。 - mkb
    @mkb 你有另一个名为“我的服务”的服务吗? - Jonathon Watney
    是的,就像问题中所述,我有一个服务,同一个可执行文件,但我想安装两个实例,每个实例都有不同的配置。我复制粘贴了服务可执行文件,但这个方法并没有起作用。 - mkb
    1
    /servicename="我的服务实例一" 和 /servicename="我的服务实例二"。这些名称必须是唯一的。 - granadaCoder

    7

    当我使用我们的自动化部署软件频繁安装/卸载并存的Windows服务时,上述方法都不太奏效。但最终我想出了以下方法,在命令行中传递一个参数以指定服务名称后缀。这个方法也可以让设计师正常工作,并且如果需要,可以轻松地调整为覆盖整个名称。

    public partial class ProjectInstaller : System.Configuration.Install.Installer
    {
      protected override void OnBeforeInstall(IDictionary savedState)
      {
        base.OnBeforeInstall(savedState);
        SetNames();
      }
    
      protected override void OnBeforeUninstall(IDictionary savedState)
      {
        base.OnBeforeUninstall(savedState);
        SetNames();
      }
    
      private void SetNames()
      {
        this.serviceInstaller1.DisplayName = AddSuffix(this.serviceInstaller1.DisplayName);
        this.serviceInstaller1.ServiceName = AddSuffix(this.serviceInstaller1.ServiceName);
      }
    
      private string AddSuffix(string originalName)
      {
        if (!String.IsNullOrWhiteSpace(this.Context.Parameters["ServiceSuffix"]))
          return originalName + " - " + this.Context.Parameters["ServiceSuffix"];
        else
          return originalName;
      }
    }
    

    考虑到这一点,我可以做以下事情:如果我已经称呼服务为“牛逼服务”,那么我可以安装服务的UAT版本如下: InstallUtil.exe /ServiceSuffix="UAT" MyService.exe 这将创建服务名称为“牛逼服务 - UAT”的服务。我们使用它来在单台机器上同时运行DEVINT、测试和验收版本的同一服务。每个版本都有自己的文件/配置-我还没有尝试使用相同的文件集安装多个服务。
    注意:您必须使用相同的/ServiceSuffix参数卸载服务,因此您需要执行以下操作进行卸载: InstallUtil.exe /u /ServiceSuffix="UAT" MyService.exe

    这很好,但那只是针对安装程序的。一旦你有了一个新的实例名称,Windows服务如何知道这个新名称呢?你必须在Windows服务构建时传递它吗? - progLearner
    在我的答案中,命令行中使用的命令用于安装(和卸载)服务到外部世界。您传递到 /ServiceSuffix="UAT" 中的值将由安装程序用于设置服务的后缀。在我的示例中,传递的值为 UAT。在我的场景中,我只想在现有服务名称上添加后缀,但您也可以根据传递的值将其替换为完整的名称。 - tristankoffee
    谢谢,但那是命令行输入(=手动输入),不是代码。根据原始问题:一旦您有了新的实例名称,Windows服务将如何知道这个新名称?您必须在构建Windows服务时传递它吗? - progLearner
    谢谢@tristankoffee。只是你的回答不完整,因为仅仅将名称传递给安装程序并不能使服务与该实例名称一起工作。我必须将该实例名称传递给我的Windows服务构造函数(在安装程序中),以便系统实际上知道并能够引用/使用该特定服务实例。否则,它将无法正常工作。除非您有另一个想法来完成它,这也是我询问的原因,因为我不确定这是否是最佳方法 :-) - progLearner
    我理解并感激你的尝试,无论如何还是谢谢你 :-) 对你的原始答案点赞。 - progLearner
    显示剩余4条评论

    4
    我为了让这个方法生效,将服务名称和显示名称存储在我的服务的app.config中。然后在我的安装程序类中,我将app.config作为XmlDocument加载,并使用xpath获取值并应用到ServiceInstaller.ServiceName和ServiceInstaller.DisplayName中,在调用InitializeComponent()之前。这假设您没有在InitializeComponent()中已经设置了这些属性,在这种情况下,来自配置文件的设置将被忽略。以下是我从我的安装程序类构造函数调用的代码,在调用InitializeComponent()之前进行:
           private void SetServiceName()
           {
              string configurationFilePath = Path.ChangeExtension(Assembly.GetExecutingAssembly().Location, "exe.config");
              XmlDocument doc = new XmlDocument();
              doc.Load(configurationFilePath);
    
              XmlNode serviceName = doc.SelectSingleNode("/xpath/to/your/@serviceName");
              XmlNode displayName = doc.SelectSingleNode("/xpath/to/your/@displayName");
    
              if (serviceName != null && !string.IsNullOrEmpty(serviceName.Value))
              {
                  this.serviceInstaller.ServiceName = serviceName.Value;
              }
    
              if (displayName != null && !string.IsNullOrEmpty(displayName.Value))
              {
                  this.serviceInstaller.DisplayName = displayName.Value;
              }
          }
    

    我不相信直接从ConfigurationManager.AppSettings或类似的地方读取配置文件会起作用,因为当安装程序运行时,它是在InstallUtil.exe的上下文中运行,而不是您的服务的.exe。您可能可以使用ConfigurationManager.OpenExeConfiguration来做一些事情,但在我的情况下,这并没有起作用,因为我试图访问未加载的自定义配置部分。


    嗨,Chris House!我偶然发现了你的答案,因为我正在构建一个基于OWIN的自托管Web API,围绕着Quartz.NET调度程序,并将其放入Windows服务中。非常棒!希望你一切都好! - NovaJoe
    嗨,Chris House!我偶然发现了你的答案,因为我正在构建一个基于OWIN的自托管Web API,围绕着Quartz.NET调度程序,并将其放入Windows服务中。非常棒!希望你一切都好! - NovaJoe

    4

    为了完善@chris.house.00的完美答案(这里),您可以考虑使用以下函数从应用程序设置中读取:

     public void GetServiceAndDisplayName(out string serviceNameVar, out string displayNameVar)
            {
                string configurationFilePath = Path.ChangeExtension(Assembly.GetExecutingAssembly().Location, "exe.config");
                XmlDocument doc = new XmlDocument();
                doc.Load(configurationFilePath);
    
                XmlNode serviceName = doc.SelectSingleNode("//appSettings//add[@key='ServiceName']");
                XmlNode displayName = doc.SelectSingleNode("//appSettings//add[@key='DisplayName']");
    
    
                if (serviceName != null && (serviceName.Attributes != null && (serviceName.Attributes["value"] != null)))
                {
                    serviceNameVar = serviceName.Attributes["value"].Value;
                }
                else
                {
                    serviceNameVar = "Custom.Service.Name";
                }
    
                if (displayName != null && (displayName.Attributes != null && (displayName.Attributes["value"] != null)))
                {
                    displayNameVar = displayName.Attributes["value"].Value;
                }
                else
                {
                    displayNameVar = "Custom.Service.DisplayName";
                }
            }
    

    2
    我曾经遇到类似的情况,在同一台服务器上需要同时运行先前的服务和更新后的服务(这不仅仅是数据库更改,还包括代码变更)。因此,我不能只运行相同的 .exe 两次。我需要一个新的 .exe,它使用新的 DLL 编译,但源自同一个项目。仅更改服务名称和显示名称并不能解决问题,我仍然收到“服务已经存在”的错误提示,我认为这是因为我正在使用部署项目。最终解决问题的方法是在我的部署项目属性中有一个名为“ProductCode”的属性,它是一个 GUID。

    enter image description here

    之后,重新构建安装项目为新的 .exe 或 .msi,安装成功。

    这是问题的实际答案。 - user15719632

    1
    最简单的方法是基于dll名称来命名服务名称:
    string sAssPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
    string sAssName = System.IO.Path.GetFileNameWithoutExtension(sAssPath);
    if ((this.ServiceInstaller1.ServiceName != sAssName)) {
        this.ServiceInstaller1.ServiceName = sAssName;
        this.ServiceInstaller1.DisplayName = sAssName;
    }
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接