views:

2618

answers:

5

Hello there,

I am currently writing a little windows service application and I can successfully in/uninstall it etc via something like this:

        serviceProcessInstaller = new ServiceProcessInstaller();
        serviceInstaller = new System.ServiceProcess.ServiceInstaller();
        serviceProcessInstaller.Account = ServiceAccount.LocalSystem;
        serviceInstaller.ServiceName = "ABC";
        serviceInstaller.StartType = ServiceStartMode.Automatic;
        serviceInstaller.Description = "DEF";
        Installers.AddRange(new Installer[] { serviceProcessInstaller, serviceInstaller });

... but I apparently cannot set the startup parameters there... or can I? I'd rather not go ahead and modify the registry later on.. therefore the Question... is there any way I can set these parameters programatically?

+5  A: 

The parameters can be set by P/Invoking the ChangeServiceConfig API. They appear after the quoted path and file name to your executable in the lpBinaryPathName argument.

The parameters will be made available to your service when it starts through the Main method:

static void Main(string[] args)

(Main is traditionally located in a file called Program.cs).

The following shows how you might modify an installer to call this API after the normal service installation logic runs. The parts of this that you most likely will need to modify are in the constructor.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration.Install;
using System.ComponentModel;
using System.Configuration.Install;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.ServiceProcess;
using System.Text;

namespace ServiceTest
{
    [RunInstaller(true)]
    public class ProjectInstaller : Installer
    {
        private string _Parameters;

        private ServiceProcessInstaller _ServiceProcessInstaller;
        private ServiceInstaller _ServiceInstaller;

        public ProjectInstaller()
        {
            _ServiceProcessInstaller = new ServiceProcessInstaller();
            _ServiceInstaller = new ServiceInstaller();

            _ServiceProcessInstaller.Account = ServiceAccount.LocalService;
            _ServiceProcessInstaller.Password = null;
            _ServiceProcessInstaller.Username = null;

            _ServiceInstaller.ServiceName = "Service1";

            this.Installers.AddRange(new System.Configuration.Install.Installer[] {
                _ServiceProcessInstaller,
                _ServiceInstaller});

            _Parameters = "/ThisIsATest";
        }

        public override void Install(IDictionary stateSaver)
        {
            base.Install(stateSaver);

            IntPtr hScm = OpenSCManager(null, null, SC_MANAGER_ALL_ACCESS);
            if (hScm == IntPtr.Zero)
                throw new Win32Exception();
            try
            {  
                IntPtr hSvc = OpenService(hScm, this._ServiceInstaller.ServiceName, SERVICE_ALL_ACCESS);
                if (hSvc == IntPtr.Zero)
                    throw new Win32Exception();
                try
                {
                    QUERY_SERVICE_CONFIG oldConfig;
                    uint bytesAllocated = 8192; // Per documentation, 8K is max size.
                    IntPtr ptr = Marshal.AllocHGlobal((int)bytesAllocated); 
                    try
                    {
                        uint bytesNeeded;
                        if (!QueryServiceConfig(hSvc, ptr, bytesAllocated, out bytesNeeded))
                        {
                            throw new Win32Exception();
                        }
                        oldConfig = (QUERY_SERVICE_CONFIG) Marshal.PtrToStructure(ptr, typeof(QUERY_SERVICE_CONFIG));
                    }
                    finally
                    {
                        Marshal.FreeHGlobal(ptr);
                    }

                    string newBinaryPathAndParameters = oldConfig.lpBinaryPathName + " " + _Parameters;

                    if (!ChangeServiceConfig(hSvc, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE,
                        newBinaryPathAndParameters, null, IntPtr.Zero, null, null, null, null))
                        throw new Win32Exception();
                }
                finally
                {
                    if (!CloseServiceHandle(hSvc))
                        throw new Win32Exception();
                }
            }
            finally
            {
                if (!CloseServiceHandle(hScm))
                    throw new Win32Exception();
            }
        }

        [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
        private static extern IntPtr OpenSCManager(
            string lpMachineName,
            string lpDatabaseName,
            uint dwDesiredAccess);

        [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
        private static extern IntPtr OpenService(
            IntPtr hSCManager,
            string lpServiceName,
            uint dwDesiredAccess);

        [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
        private struct QUERY_SERVICE_CONFIG {
            public uint dwServiceType;   
            public uint dwStartType;
            public uint dwErrorControl;
            public string lpBinaryPathName;
            public string lpLoadOrderGroup;
            public uint dwTagId;
            public string lpDependencies;
            public string lpServiceStartName;
            public string lpDisplayName;
        }

        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool QueryServiceConfig(
            IntPtr hService,
            IntPtr lpServiceConfig,
            uint cbBufSize,
            out uint pcbBytesNeeded);

        [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool ChangeServiceConfig(
            IntPtr hService,
            uint dwServiceType,
            uint dwStartType,
            uint dwErrorControl,
            string lpBinaryPathName,
            string lpLoadOrderGroup,
            IntPtr lpdwTagId,
            string lpDependencies,
            string lpServiceStartName,
            string lpPassword,
            string lpDisplayName);

        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool CloseServiceHandle(
            IntPtr hSCObject);

        private const uint SERVICE_NO_CHANGE = 0xffffffffu;
        private const uint SC_MANAGER_ALL_ACCESS = 0xf003fu;
        private const uint SERVICE_ALL_ACCESS = 0xf01ffu;
    }
}
binarycoder
Awesome - not as elegantly as I hoped it would be, but thanks!
Jörg B.
+1 Nicely done!
Anton Gogolev
A: 

This is not possible to do in managed code.

But there is one decent solution though. If all you want is having the same executable for windows service and GUI (most common scenario). You don't even need parameters. Just check in Main method for System.Environment.UserInteractive property and decide what to do...

static void Main(string[] args)
{
    if (System.Environment.UserInteractive)
    {
        // start your app normally
    }
    else
    {
        // start your windows sevice
    }
}
lubos hasko
Ooooh! I didn't know that... thank you!
Jörg B.
A: 

For some strange reason my QUERY_SERVICE_CONFIG struct wasn't getting the full value of lpBinaryPathName, only the first character. Changing it to the class below seemed to fix the problem. There is complete code at http://www.pinvoke.net/default.aspx/advapi32/QueryServiceConfig.html

Edit: Also note this sets the "Path to executable" of the windows service but does not set the "Start parameters" of the windows service.

[StructLayout(LayoutKind.Sequential)]
public class QUERY_SERVICE_CONFIG
{
    [MarshalAs(System.Runtime.InteropServices.UnmanagedType.U4)]
    public UInt32 dwServiceType;
    [MarshalAs(System.Runtime.InteropServices.UnmanagedType.U4)]
    public UInt32 dwStartType;
    [MarshalAs(System.Runtime.InteropServices.UnmanagedType.U4)]
    public UInt32 dwErrorControl;
    [MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)]
    public String lpBinaryPathName;
    [MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)]
    public String lpLoadOrderGroup;
    [MarshalAs(System.Runtime.InteropServices.UnmanagedType.U4)]
    public UInt32 dwTagID;
    [MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)]
    public String lpDependencies;
    [MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)]
    public String lpServiceStartName;
    [MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)]
    public String lpDisplayName;
}
Axl
+1  A: 

There is a managed way to add start parameters to a services (not in the "Start parameters"/"Startparameter" section of the management console (services.msc) but in "Path to executable"/"Pfad zur EXE-Datei" like all the windows' native services do).

Add the following code to your subclass of System.Configuration.Install.Installer: (in C#-friendly VB-Code)

'Just as sample
Private _CommandLineArgs As String() = New String() {"/Debug", "/LogSection:Hello World"}

''' <summary>Command line arguments without double-quotes.</summary>
Public Property CommandLineArgs() As String()
  Get
    Return _CommandLineArgs
  End Get
  Set(ByVal value As String())
    _CommandLineArgs = value
  End Set
End Property

Public Overrides Sub Install(ByVal aStateSaver As System.Collections.IDictionary)
  Dim myPath As String = GetPathToExecutable()
  Context.Parameters.Item("assemblypath") = myPath
  MyBase.Install(aStateSaver)
End Sub

Private Function GetPathToExecutable() As String
  'Format as something like 'MyService.exe" "/Test" "/LogSection:Hello World'
  'Hint: The base class (System.ServiceProcess.ServiceInstaller) adds simple-mindedly
  '      a double-quote around this string that's why we have to omit it here.
  Const myDelimiter As String = """ """ 'double-quote space double-quote
  Dim myResult As New StringBuilder(Context.Parameters.Item("assemblypath"))
  myResult.Append(myDelimiter)
  myResult.Append(Microsoft.VisualBasic.Strings.Join(CommandLineArgs, myDelimiter))
  Return myResult.ToString()
End Function

Have fun!

chha

chha
A: 

I have found a way to add start parameters on service installation:

http://stackoverflow.com/questions/200163/am-i-running-as-a-service/2111492#2111492

Rolf Kristensen