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

因此,我们已经生成了一个 Windows 服务,将数据提供给我们的客户端应用程序,一切都进展顺利。客户机提出了一个有趣的配置请求,该请求要求此服务的两个实例在同一台服务器上运行,并配置为指向不同的数据库。

到目前为止,我还没有能够让这种情况发生,并希望我的堆栈溢出成员可以提供一些线索,为什么。

当前设置:

我已经设置了包含 windows 服务的项目,从现在开始我们称之为 AppService,以及处理自定义安装步骤的 projectinstaller.cs 文件,它根据 App.config 中的一个键来设置服务名称,如下所示:

this.serviceInstaller1.ServiceName = Util.ServiceName;
this.serviceInstaller1.DisplayName = Util.ServiceName;
this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.LocalSystem;

在这种情况下,Util 只是一个静态类,它从配置文件中加载服务名称。

从现在开始,我尝试了两种不同的方法来安装这两个服务,但都以同样的方式失败了。

第一种方法是简单地安装服务的第一个副本,复制已安装的目录并重命名它,然后在修改 app 配置以更改所需的服务名称之后运行以下命令:

InstallUtil.exe /i AppService.exe

当这不起作用时,我尝试创建第二个安装程序项目,编辑配置文件并构建第二个安装程序。当我运行安装程序时,它工作得很好,但是在 services.msc 中没有显示服务,所以我对第二个安装的代码库运行了前面的命令。

两次我都收到了 InstallUtil 的以下输出(只有相关部分) :

运行事务处理安装。

开始安装的安装阶段。

安装服务应用程序服务二..。 ServiceAppServiceTwo 已成功安装。 在日志应用程序中创建 EventLog 源 AppServiceTwo..。

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

安装的回滚阶段已经开始。

将源 AppServiceTwo 的事件日志还原到以前的状态。 服务应用程序服务二正在从系统中删除..。 ServiceAppServiceTwo 已成功从系统中删除。

回滚阶段成功完成。

事务处理的安装已经完成。 安装失败,已执行回滚。

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

109012 次浏览

You can run multiple versions of the same service by doing the following:

1) Copy the Service executable and config to its own folder.

2) Copy Install.Exe to the service executable folder (from .net framework folder)

3) Create a config file called Install.exe.config in the service executable folder with the following contents (unique service names):

<?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) Create a batch file to install the service with the following contents:

REM Install
InstallUtil.exe YourService.exe
pause

5) While your there, create an uninstall batch file

REM Uninstall
InstallUtil.exe -u YourService.exe
pause

EDIT:

Note sure if I missed something, here is the ServiceInstaller Class (adjust as required):

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

Have you tried the sc / service controller util? Type

sc create

at a command line, and it will give you the help entry. I think I've done this in the past for Subversion and used this article as a reference:

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

What I've done to make this work is to store the service name and display name in an app.config for my service. Then in my installer class, I load the app.config as an XmlDocument and use xpath to get the values out and apply them to ServiceInstaller.ServiceName and ServiceInstaller.DisplayName, before calling InitializeComponent(). This assumes you're not already setting these properties in InitializeComponent(), in which case, the settings from your config file will be ignored. The following code is what I'm calling from my installer class constructor, before 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;
}
}

I don't believe reading the configuration file directly from ConfigurationManager.AppSettings or something similar will work as when the installer runs, it's run in the context of InstallUtil.exe, not your service's .exe. You may be able to do something with ConfigurationManager.OpenExeConfiguration, however in my case, this didn't work as I was trying to get at a custom configuration section that was not loaded.

Old question, I know, but I've had luck using the /servicename option on InstallUtil.exe. I don't see it listed in the built-in help though.

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

I'm not entirely sure where I first read about this but I haven't seen it since. YMMV.

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

This solution worked for me.

Just to improve perfect answer of @chris.house.00 this, you can consider following function to read from your app settings:

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

I didn't have much luck with the above methods when using our automated deployment software to frequently install/uninstall side-by-side windows services, but I eventually came up with the following which allows me to pass in a parameter to specify a suffix to the service name on the command line. It also allows the designer to function properly and could easily be adapted to override the entire name if necessary.

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

With this in mind, I can do the following: If I've called the service "Awesome Service" then I can install a UAT verison of the service as follows:

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

This will create the service with the name "Awesome Service - UAT". We've used this to run DEVINT, TESTING and ACCEPTANCE versions of the same service running side-by-side on a single machine. Each version has its own set of files/configs - I haven't tried this to install multiple services pointing at the same set of files.

NOTE: you have to use the same /ServiceSuffix parameter to uninstall the service, so you'd execute the following to uninstall:

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

Another quick way to specify a custom value for ServiceName and DisplayName is using installutil command line parameters.

  1. In your ProjectInstaller class override virtual methods Install(IDictionary stateSaver) and 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. Build your project
  3. Install the service with installutil adding your custom name using /servicename parameter:

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

Please note that if you do not specify /servicename in the command line the service will be installed with ServiceName and DisplayName values specified in ProjectInstaller properties/config

I had a similar situation, where i to needed have a previous service, and an updated service running side by side on the same server. (It was more than just a database change, it was code changes as well). So I couldn't just run the same .exe twice. I needed a new .exe that was compiled with new DLLs but from the same project. Just changing the service name and display name of the service did not work for me, I still received the "service already existed error" which I believe is because I am using a Deployment Project. What finally did work for me is within my Deployment Project Properties there is a property called "ProductCode" which is a Guid.

enter image description here

After that, rebuilding the Setup Project to a new .exe or .msi installed successfully.

The simplest approach is is based the service name on the dll name:

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