#region Copyright 2011-2014 by Roger Knapp, Licensed under the Apache License, Version 2.0 /* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #endregion using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Security.Principal; using System.Security.AccessControl; using System.ServiceProcess; using System.ComponentModel; using System.Threading; using CSharpTest.Net.Utils; namespace CSharpTest.Net.Services { /// /// Advanced Service Control for installation/uninstallation and security settings /// public class SvcControlManager : IDisposable { private const string UnableToControlService = "The service did not respond."; /// /// Represets common NT_AUTHORITY service accounts that do not require a password /// at install time. /// public static class NT_AUTHORITY { /// NT_AUTHORITY\LocalSystem public const string LocalSystem = null; /// NT_AUTHORITY\LocalService public const string LocalService = @"NT AUTHORITY\LocalService"; /// NT_AUTHORITY\NetworkService public const string NetworkService = @"NT AUTHORITY\NetworkService"; /// Selects the account bysed on the System.ServiceProcess.ServiceAccount enumeration public static string Account(ServiceAccount account) { switch (account) { case ServiceAccount.LocalService: return LocalService; case ServiceAccount.LocalSystem: return LocalSystem; case ServiceAccount.NetworkService: return NetworkService; default: throw new ArgumentOutOfRangeException( "The service account must be one of LocalService, NetworkService, or LocalSystem.", "account"); } } } private readonly string _svcName; /// /// Constructs the SvcControlManager for the service name provided. /// public SvcControlManager(string serviceName) { _svcName = serviceName; } /// /// Disposes of the SvcControlManager /// public void Dispose() { } /// /// Starts the service with the arguments specified and waits for the service /// to enter the running state. /// public void Start(string[] arguments) { using (var sc = new ServiceController(_svcName)) { if (sc.Status == ServiceControllerStatus.Running) return; sc.Start(arguments ?? new string[0]); do { Thread.Sleep(1000); sc.Refresh(); } while (sc.Status == ServiceControllerStatus.StartPending); if (sc.Status != ServiceControllerStatus.Running) throw new ApplicationException(UnableToControlService); } } /// /// Stops the service and waits for the service to enter the Stopped state. /// public void Stop() { using (var sc = new ServiceController(_svcName)) { if (sc.Status == ServiceControllerStatus.Stopped) return; sc.Stop(); do { Thread.Sleep(1000); sc.Refresh(); } while (sc.Status == ServiceControllerStatus.StopPending); if (sc.Status != ServiceControllerStatus.Stopped) throw new ApplicationException(UnableToControlService); } } /// /// Configures the service to use the delayed auto-start policy /// public void SetDelayAutostart(bool enabled) { SetServiceConfig(SERVICE_CONFIG_INFO.DELAYED_AUTO_START_INFO, enabled ? 1 : 0); } /// /// Sets the service's default shutdown timeout period. /// public void SetShutdownTimeout(TimeSpan timeoutValue) { SetServiceConfig(SERVICE_CONFIG_INFO.PRESHUTDOWN_INFO, (int)timeoutValue.TotalMilliseconds); } /// /// Sets the description text of the service. /// public void SetDescription(string description) { GCHandle hdata = GCHandle.Alloc(description, GCHandleType.Pinned); try { SC_DESCRIPTION desc = new SC_DESCRIPTION(); desc.Description = hdata.AddrOfPinnedObject(); SetServiceConfig(SERVICE_CONFIG_INFO.DESCRIPTION, desc); } finally { hdata.Free(); } } void SetServiceConfig(SERVICE_CONFIG_INFO infoId, T objData) { GCHandle hdata = GCHandle.Alloc(objData, GCHandleType.Pinned); try { WithServiceHandle( ServiceAccessRights.GENERIC_READ | ServiceAccessRights.GENERIC_WRITE, delegate(IntPtr svcHandle) { if (0 == Win32.ChangeServiceConfig2(svcHandle, (int)infoId, hdata.AddrOfPinnedObject())) throw new Win32Exception(); } ); } finally { hdata.Free(); } } /// /// Changes the service's executable and arguments /// public void SetServiceExeArgs(string exePath, string[] arguments) { exePath = ArgumentList.EscapeArguments(new string[] {Check.NotEmpty(exePath)}); if (arguments != null && arguments.Length > 0) exePath = String.Format("{0} {1}", exePath, ArgumentList.EscapeArguments(arguments)); WithServiceHandle( ServiceAccessRights.GENERIC_READ | ServiceAccessRights.GENERIC_WRITE, delegate(IntPtr svcHandle) { const int notChanged = -1; if (0 == Win32.ChangeServiceConfig(svcHandle, notChanged, notChanged, notChanged, exePath, null, IntPtr.Zero, null, null, null, null)) throw new Win32Exception(); } ); } /// /// Configures the service to auto-restart on failure /// public void SetRestartOnFailure(int restartAttempts, int restartDelay, int resetFailuresDelay) { SC_ACTION[] actions = new SC_ACTION[3] { new SC_ACTION { Delay = Math.Max(0, Math.Min(int.MaxValue, restartDelay)), Type = SC_ACTION_TYPE.SC_ACTION_RESTART }, new SC_ACTION { Delay = Math.Max(0, Math.Min(int.MaxValue, restartDelay)), Type = SC_ACTION_TYPE.SC_ACTION_RESTART }, new SC_ACTION { Delay = Math.Max(0, Math.Min(int.MaxValue, restartDelay)), Type = SC_ACTION_TYPE.SC_ACTION_RESTART }, }; for (int i = Math.Max(0, restartAttempts); i < actions.Length; i++) actions[i] = new SC_ACTION { Delay = 0, Type = SC_ACTION_TYPE.SC_ACTION_NONE }; GCHandle hdata = GCHandle.Alloc(actions, GCHandleType.Pinned); try { SERVICE_FAILURE_ACTIONS cfg = new SERVICE_FAILURE_ACTIONS(); cfg.dwResetPeriod = Math.Max(-1, Math.Min(int.MaxValue, resetFailuresDelay)); cfg.lpRebootMsg = cfg.lpCommand = IntPtr.Zero; cfg.cActions = actions.Length; cfg.lpsaActions = hdata.AddrOfPinnedObject(); SetServiceConfig(SERVICE_CONFIG_INFO.FAILURE_ACTIONS, cfg); } finally { hdata.Free(); } } /// /// Replaces the access control list for the service. /// public void SetAccess(IEnumerable aces) { uint bufSizeNeeded; byte[] psd = new byte[0]; WithServiceHandle( ServiceAccessRights.SERVICE_ALL_ACCESS, delegate(IntPtr svcHandle) { Win32.QueryServiceObjectSecurity(svcHandle, SecurityInfos.DiscretionaryAcl, psd, 0, out bufSizeNeeded); if (bufSizeNeeded < 0 || bufSizeNeeded > short.MaxValue) throw new Win32Exception(); if (!Win32.QueryServiceObjectSecurity(svcHandle, SecurityInfos.DiscretionaryAcl, psd = new byte[bufSizeNeeded], bufSizeNeeded, out bufSizeNeeded)) throw new Win32Exception(); } ); RawSecurityDescriptor rsd = new RawSecurityDescriptor(psd, 0); while (rsd.DiscretionaryAcl.Count > 0) rsd.DiscretionaryAcl.RemoveAce(0); rsd.DiscretionaryAcl.InsertAce(rsd.DiscretionaryAcl.Count, new CommonAce(AceFlags.None, AceQualifier.AccessAllowed, (int)ServiceAccessRights.SERVICE_ALL_ACCESS, new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), false, null)); foreach (ServiceAccessEntry ace in aces) { SecurityIdentifier sid = new SecurityIdentifier(ace.Sid, null); rsd.DiscretionaryAcl.InsertAce(rsd.DiscretionaryAcl.Count, new CommonAce(AceFlags.None, ace.Qualifier, (int)ace.AccessMask, sid, false, null)); } byte[] rawsd = new byte[rsd.BinaryLength]; rsd.GetBinaryForm(rawsd, 0); WithServiceHandle( ServiceAccessRights.SERVICE_ALL_ACCESS, delegate(IntPtr svcHandle) { if (!Win32.SetServiceObjectSecurity(svcHandle, SecurityInfos.DiscretionaryAcl, rawsd)) throw new Win32Exception(); } ); } /// Creates the specified service and returns a SvcControlManager for the service created public static SvcControlManager Create(string serviceName, string displayName, bool interactive, ServiceStartMode startupType, string exePath, string[] arguments, string accountName, string password) { exePath = ArgumentList.EscapeArguments(new string[] { Check.NotEmpty(exePath) }); if (arguments != null && arguments.Length > 0) exePath = String.Format("{0} {1}", exePath, ArgumentList.EscapeArguments(arguments)); using (SCMHandle hScm = new SCMHandle(SCM_ACCESS.SC_MANAGER_CREATE_SERVICE)) { IntPtr hSvc = Win32.CreateService( hScm, serviceName, displayName ?? serviceName, ServiceAccessRights.SERVICE_ALL_ACCESS, SC_SERVICE_TYPE.SERVICE_WIN32_OWN_PROCESS | (interactive ? SC_SERVICE_TYPE.SERVICE_INTERACTIVE_PROCESS : 0), startupType, SC_SERVICE_ERROR_CONTROL.SERVICE_ERROR_NORMAL, exePath, null, null, null, accountName, password); if (hSvc == IntPtr.Zero) throw new Win32Exception(); Win32.CloseServiceHandle(hSvc); } return new SvcControlManager(serviceName); } /// /// Stops/Deletes the specified service /// public void Delete() { try { Stop(); } finally { WithServiceHandle( ServiceAccessRights.GENERIC_READ | ServiceAccessRights.DELETE, delegate(IntPtr hSvc) { if (!Win32.DeleteService(hSvc)) throw new Win32Exception(); } ); } } private void WithServiceHandle(ServiceAccessRights access, Action action) { using (SCMHandle hScm = new SCMHandle(SCM_ACCESS.STANDARD_RIGHTS_REQUIRED)) { IntPtr hSvc = Win32.OpenService(hScm, _svcName, access); if (hSvc == IntPtr.Zero) throw new Win32Exception(); try { action(hSvc); } finally { Win32.CloseServiceHandle(hSvc); } } } #region WIN32 Service Methods [Flags] enum SCM_ACCESS : uint { STANDARD_RIGHTS_REQUIRED = 0xF0000, SC_MANAGER_CONNECT = 0x00001, SC_MANAGER_CREATE_SERVICE = 0x00002, SC_MANAGER_ENUMERATE_SERVICE = 0x00004, SC_MANAGER_LOCK = 0x00008, SC_MANAGER_QUERY_LOCK_STATUS = 0x00010, SC_MANAGER_MODIFY_BOOT_CONFIG = 0x00020, SC_MANAGER_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SC_MANAGER_CONNECT | SC_MANAGER_CREATE_SERVICE | SC_MANAGER_ENUMERATE_SERVICE | SC_MANAGER_LOCK | SC_MANAGER_QUERY_LOCK_STATUS | SC_MANAGER_MODIFY_BOOT_CONFIG } private class SCMHandle : SafeHandle { public SCMHandle(SCM_ACCESS rights) : base(Win32.OpenSCManager(null, null, rights), true) { if (this.handle == IntPtr.Zero) { GC.SuppressFinalize(this); throw new Win32Exception(); } } public override bool IsInvalid { get { return handle == IntPtr.Zero; } } protected override bool ReleaseHandle() { return Win32.CloseServiceHandle(handle); } } private enum SERVICE_CONFIG_INFO { DESCRIPTION = 1, FAILURE_ACTIONS = 2, DELAYED_AUTO_START_INFO = 3, FAILURE_ACTIONS_FLAG = 4, SERVICE_SID_INFO = 5, REQUIRED_PRIVILEGES_INFO = 6, PRESHUTDOWN_INFO = 7 } private enum SC_ACTION_TYPE : uint { SC_ACTION_NONE = 0x00000000, // No action. SC_ACTION_RESTART = 0x00000001, // Restart the service. SC_ACTION_REBOOT = 0x00000002, // Reboot the computer. SC_ACTION_RUN_COMMAND = 0x00000003 // Run a command. } [Flags] private enum SC_SERVICE_TYPE : uint { SERVICE_ADAPTER = 0x00000004, SERVICE_FILE_SYSTEM_DRIVER = 0x00000002, SERVICE_KERNEL_DRIVER = 0x00000001, SERVICE_RECOGNIZER_DRIVER = 0x00000008, SERVICE_WIN32_OWN_PROCESS = 0x00000010, SERVICE_WIN32_SHARE_PROCESS = 0x00000020, SERVICE_INTERACTIVE_PROCESS = 0x00000100, } private struct SERVICE_FAILURE_ACTIONS { public Int32 dwResetPeriod; public IntPtr lpRebootMsg; public IntPtr lpCommand; public Int32 cActions; public IntPtr lpsaActions; } private struct SC_DESCRIPTION { public IntPtr Description; } private struct SC_ACTION { public SC_ACTION_TYPE Type; public Int32 Delay; } private enum SC_SERVICE_ERROR_CONTROL { SERVICE_ERROR_IGNORE = 0x00000000, //The startup program ignores the error and continues the startup operation. SERVICE_ERROR_NORMAL = 0x00000001, //The startup program logs the error in the event log but continues the startup operation. SERVICE_ERROR_SEVERE = 0x00000002, //The startup program logs the error in the event log. If the last-known-good configuration is being started, the startup operation continues. Otherwise, the system is restarted with the last-known-good configuration. } private static class Win32 { [DllImport("advapi32.dll", SetLastError = true)] public static extern bool SetServiceObjectSecurity(IntPtr serviceHandle, SecurityInfos secInfos, [In] byte[] lpSecDesrBuf); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool QueryServiceObjectSecurity(IntPtr serviceHandle, SecurityInfos secInfo, [Out] byte[] lpSecDesrBuf, uint bufSize, out uint bufSizeNeeded); [DllImport("advapi32.dll", EntryPoint = "ChangeServiceConfig2W", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)] public static extern int ChangeServiceConfig2(IntPtr hService, int dwInfoLevel, IntPtr lpInfo); [DllImport("advapi32.dll", EntryPoint = "ChangeServiceConfigW", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)] public static extern int ChangeServiceConfig(IntPtr hService, int nServiceType, int nStartType, int nErrorControl, String lpBinaryPathName, String lpLoadOrderGroup, IntPtr lpdwTagId, [In] String lpDependencies, String lpServiceStartName, String lpPassword, String lpDisplayName); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern IntPtr OpenService(SCMHandle hSCManager, string lpServiceName, ServiceAccessRights dwDesiredAccess); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool DeleteService(IntPtr hService); [DllImport("advapi32.dll", EntryPoint="OpenSCManagerW", ExactSpelling=true, CharSet=CharSet.Unicode, SetLastError=true)] public static extern IntPtr OpenSCManager(string machineName, string databaseName, SCM_ACCESS dwAccess); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool CloseServiceHandle(IntPtr hSCObject); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern IntPtr CreateService( SCMHandle hSCManager, string lpServiceName, string lpDisplayName, ServiceAccessRights dwDesiredAccess, SC_SERVICE_TYPE dwServiceType, ServiceStartMode dwStartType, SC_SERVICE_ERROR_CONTROL dwErrorControl, string lpBinaryPathName, string lpLoadOrderGroup, string lpdwTagId, string lpDependencies, string lpServiceStartName, string lpPassword); } #endregion } }