【问题标题】:Installing multiple instances of the same windows service on a server在服务器上安装同一 Windows 服务的多个实例
【发布时间】:2010-11-19 17:54:21
【问题描述】:

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

到目前为止,我还无法实现这一点,并希望我的 * 成员能够就原因提供一些提示。

当前设置:

我已经设置了包含 windows 服务的项目,从现在开始我们将其称为 AppService,以及处理自定义安装步骤的 ProjectInstaller.cs 文件,以根据 App 中的键设置服务名称。配置如下:

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 收到以下输出(仅相关部分):

运行事务安装。

开始安装的安装阶段。

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

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

安装的回滚阶段开始。

将源应用服务二的事件日志恢复到以前的状态。 正在从系统中删除服务应用服务二... 服务应用服务二已成功从系统中删除。

回滚阶段成功完成。

事务安装已完成。 安装失败,已回滚。

很抱歉这篇冗长的帖子,想确保有足够的相关信息。到目前为止,让我感到困惑的是,它表明服务的安装成功完成,并且只有在它创建 EventLog 源之后才会抛出 NullReferenceException。因此,如果有人知道我做错了什么或有更好的方法,将不胜感激。

【问题讨论】:

    标签: windows windows-services windows-installer installutil


    【解决方案1】:

    您是否尝试过 sc / 服务控制器实用程序?类型

    sc create
    

    在命令行中,它会为您提供帮助条目。我想我过去曾为 Subversion 做过此操作,并使用 this article 作为参考:

    http://svn.apache.org/repos/asf/subversion/trunk/notes/windows-service.txt

    【讨论】:

    • 我发现这个页面很有用:http://journalofasoftwaredev.wordpress.com/2008/07/16/multiple-instances-of-same-windows-service/。您可以在安装程序中插入代码以在运行 installutil 时获取所需的服务名称。
    • wordpress 博客的链接已更改为:journalofasoftwaredev.wordpress.com/2008/07
    【解决方案2】:
      sc create [servicename] binpath= [path to your exe]
    

    这个解决方案对我有用。

    【讨论】:

    • 只是指出; [path to your exe] 必须是完整路径,不要忘记binpath= 后面的空格
    • 这确实允许多次安装服务。但是,服务安装程序提供的所有信息。 F.e.描述、登录类型等被忽略
    【解决方案3】:

    您可以通过执行以下操作来运行同一服务的多个版本:

    1) 将服务可执行文件和配置复制到自己的文件夹中。

    2) 将 Install.Exe 复制到服务可执行文件夹(来自 .net 框架文件夹)

    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());
                }
            }
        }
    }
    

    【讨论】:

    • 我认为您所描述的或多或少是我通过允许从我的服务 app.config 设置 ServiceName 和 DisplayName 所做的我确实尝试了您所描述的,但不幸的是它导致了我的问题中列出了相同的问题。
    • 我有一个我使用的模板,我已经使用了很长时间,所以也许我错过了一些东西,你的 ServiceInstaller 类是什么样的,将发布我使用的一个的工作副本,让我知道这有帮助吗?
    • 我们的服务安装人员实际上几乎相同。我使用静态类从配置文件加载服务和显示名称,但除此之外它们非常相似。我对它为什么对我不起作用的猜测是,我们的服务代码可能有些特殊。不幸的是,很多人都参与其中。不过据我了解,您的答案在大多数情况下都应该有效,感谢您的帮助。
    • 巨大的帮助,谢谢。我认为安装配置文件需要命名为 InstallUtil.exe.confg 而不是 InstallUtil.exe 的 Install.exe.config
    • 一个很好的方法,完全有效。也就是说,如果您知道要将哪个 InstallUtil.exe 复制到您的安装文件夹(我个人安装了大量的框架版本,而 64 位副本加剧了这些版本)。如果他们进行安装,这将很难向帮助台团队解释。但对于开发人员主导的安装来说,它非常优雅。
    【解决方案4】:

    另一种为ServiceNameDisplayName 指定自定义值的快速方法是使用installutil 命令行参数。

    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;
          }
      }
      
    2. 构建您的项目
    3. 使用installutil 安装服务,使用/servicename 参数添加您的自定义名称:

      installutil.exe /servicename="CustomServiceName" "c:\pathToService\SrvcExecutable.exe"
      

    请注意,如果您未在命令行中指定 /servicename,则将使用 ProjectInstaller 属性/配置中指定的 ServiceName 和 DisplayName 值安装服务

    【讨论】:

    • 太棒了!!谢谢 - 这正是我们所需要的。
    【解决方案5】:

    老问题,我知道,但我很幸运地使用了 InstallUtil.exe 上的 /servicename 选项。不过,我没有在内置帮助中看到它。

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

    我不完全确定我是从哪里第一次读到这个的,但从那以后我就再也没有看到过。 YMMV。

    【讨论】:

    • 返回此错误:An exception occurred during the Install phase. System.ComponentModel.Win32Exception: The specified service already exists
    • @mkb 你有另一个叫“我的服务”的服务吗?
    • 是的,在问题中我有一个服务,相同的可执行文件,但我想安装它的两个实例,每个实例都有不同的配置。我复制粘贴了服务 exe,但这个没有用。
    • /servicename="My Service InstanceOne" 和 /servicename="My Service InstanceTwo" 名称必须是唯一的。
    【解决方案6】:

    在使用我们的自动部署软件频繁安装/卸载并行 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;
      }
    }
    

    考虑到这一点,我可以执行以下操作: 如果我将服务称为“Awesome Service”,那么我可以安装该服务的 UAT 版本,如下所示:

    InstallUtil.exe /ServiceSuffix="UAT" MyService.exe

    这将创建名为“Awesome Service - UAT”的服务。我们已经使用它在单台机器上并行运行同一服务的 DEVINT、TESTING 和 ACCEPTANCE 版本。每个版本都有自己的一组文件/配置 - 我还没有尝试安装多个指向同一组文件的服务。

    注意:您必须使用相同的/ServiceSuffix 参数来卸载服务,因此您需要执行以下操作来卸载:

    InstallUtil.exe /u /ServiceSuffix="UAT" MyService.exe

    【讨论】:

    • 这很好,但这仅适用于安装程序。一旦有了新的实例名称,Windows 服务将如何知道这个新名称?在构建 Windows 服务时必须传递它吗?
    • 谢谢!安装程序将在安装 Windows 服务时使用上面的 SetNames() 方法中设置的值设置名称。
    • 当然可以,但是如何从外界设置此名称?
    • 在我的回答中是在命令行上使用的命令,用于在外界安装(和卸载)服务。安装程序使用您传递给/ServiceSuffix="UAT" 的值来设置服务的后缀。在我的示例中,传入的值为UAT。在我的场景中,我只是想在服务的现有名称上添加一个后缀,但没有理由不能调整它以将名称完全替换为传入的值。
    • 谢谢,但这是命令行输入(=手动输入),而不是代码。根据最初的问题:一旦有了新的实例名称,Windows 服务将如何知道这个新名称?在构建 Windows 服务时必须传递它吗?
    【解决方案7】:

    我所做的就是将服务名称和显示名称存储在我的服务的 app.config 中。然后在我的安装程序类中,我将 app.config 作为 XmlDocument 加载,并在调用 InitializeComponent() 之前使用 xpath 获取值并将它们应用到 ServiceInstaller.ServiceName 和 ServiceInstaller.DisplayName。这假设您尚未在 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 执行某些操作,但在我的情况下,这不起作用,因为我试图获取未加载的自定义配置部分。

    【讨论】:

    • 嗨,克里斯豪斯!偶然发现了您的答案,因为我正在围绕 Quartz.NET 调度程序构建一个基于 OWIN 的自托管 Web API 并将其粘贴在 Windows 服务中。相当光滑!希望你一切都好!
    • 嗨,克里斯豪斯!偶然发现了您的答案,因为我正在围绕 Quartz.NET 调度程序构建一个基于 OWIN 的自托管 Web API 并将其粘贴在 Windows 服务中。相当光滑!希望你一切都好!
    【解决方案8】:

    为了提高@chris.house.00 this 的完美答案,您可以考虑从您的应用设置中读取以下功能:

     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";
                }
            }
    

    【讨论】:

      【解决方案9】:

      我有类似的情况,我需要有一个以前的服务,以及一个更新的服务在同一台服务器上并排运行。 (这不仅仅是数据库更改,还有代码更改)。所以我不能只运行相同的 .exe 两次。我需要一个用新 DLL 编译但来自同一个项目的新 .exe。只是更改服务的服务名称和显示名称对我不起作用,我仍然收到“服务已存在错误”,我认为这是因为我正在使用部署项目。最终对我有用的是在我的部署项目属性中,有一个名为“ProductCode”的属性,它是一个 Guid。

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

      【讨论】:

        【解决方案10】:

        最简单的方法是基于 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;
        }
        

        【讨论】: