Reputation: 116528
I have a ServiceProcessInstaller
which installs a .NET Windows Service.
The install process works perfectly if I either:
serviceProcessInstaller1.Account = ServiceAccount.LocalSystem
). serviceProcessInstaller1.Account = ServiceAccount.User
) by either specifying the Username
and Password
properties, or letting the install process prompt me.However, I would like the service to run as a virtual user, a la NT Service\ServiceName
. If you look at the some of the SQL Server services, you will see they default to log on as their own virtual user account. There is more, albeit limited, information at http://technet.microsoft.com/en-us/library/dd548356.aspx.
I have tried setting serviceProcessInstaller1.Username = @"NT Service\ServiceName"
, but the installer then throws the following error no matter what I give as the password (I have tried String.Empty
, same as the user name, my own password, null
to bring up the interactive dialog, and even random junk):
No mapping between account names and security IDs was done
However, if I install the service normally (e.g. run as SYSTEM), I can then go in to the service's properties from the services.msc
snap-in, on the Log On page change the user to NT Service\ServiceName
, and it works beautifully.
I have also looked into the ChangeServiceConfig2
function but I can't seem to get it to change anything either.
How can I set the log on user to the virtual user NT Service\ServiceName
from code within my ServiceProcessInstaller
?
Upvotes: 3
Views: 2842
Reputation: 7931
An alternative approach to the solution suggested above (using ServiceInstaller.Committed
event) is described as workaround on connect.microsoft.com. The idea is to tweak a private field haveLoginInfo
via reflection to allow null
as valid password.
const string s_ServiceName = "myservice1";
const string s_DisplayName = "Tell admin what it is";
const string s_Description = "Tell admin what it does";
var procesServiceInstaller = new ServiceProcessInstaller
{
Account = ServiceAccount.User,
Username = string.Format("NT Service\\{0}", s_ServiceName),
Password = null,
};
//Here comes the hack.
// ReSharper disable once PossibleNullReferenceException
procesServiceInstaller
.GetType()
.GetField("haveLoginInfo", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(procesServiceInstaller, true);
var serviceInstaller = new ServiceInstaller();
var path = string.Format(
"/assemblypath={0}",
Assembly.GetExecutingAssembly().Location);
string[] cmdline = { path };
var context = new InstallContext("", cmdline);
serviceInstaller.Context = context;
serviceInstaller.DisplayName = s_DisplayName;
serviceInstaller.ServiceName = s_ServiceName;
serviceInstaller.Description = s_Description;
serviceInstaller.StartType = ServiceStartMode.Manual;
serviceInstaller.Parent = procesServiceInstaller;
try
{
var state = new ListDictionary();
serviceInstaller.Install(state);
}
catch (Win32Exception win32Exception)
{
//TODO: HandleException(win32Exception);
}
catch (InvalidOperationException ex)
{
//TODO: HandleException(ex);
}
Even this solution is not less hacky, at least it's visually less ugly.
NOTE: There is a mistake in the workaround description at connect.microsoft.com. The private field name mentioned there is hasLoginInfo
but must be haveLoginInfo
.
Upvotes: 1
Reputation: 116528
You cannot do it directly with the ServiceProcessInstaller
object. However, you can set the username after the service is installed, in the ServiceInstaller.Committed
event, using the Change method in WMI. Specify the username as wmiParams[6]
and leave the password null:
void serviceInstaller1_Committed(object sender, InstallEventArgs e)
{
using (ManagementObject service = new ManagementObject(new ManagementPath("Win32_Service.Name='ServiceName'")))
{
object[] wmiParams = new object[11];
wmiParams[6] = @"NT Service\ServiceName";
service.InvokeMethod("Change", wmiParams);
}
}
Finally, don't forget to give the user read/execute permission on your service exe and config files, or you will get an Access Denied error.
Upvotes: 3