扩展的服务控制器






4.89/5 (9投票s)
.NET ServiceController 类扩展
引言
我想介绍一个 .NET (4.0 或更高版本) ServiceController 类的扩展。它的主要目的是增加一些对日常编程实践有用的功能,但遗憾的是框架本身并未提供这些功能。
ExtendedServiceController
类引入的功能包括:
- Windows 服务本地和远程启动模式配置
- Windows 服务本地和远程存在性检查
- 通过适当的连接和身份验证流程进行 Windows 服务的远程管理
背景
向 ServiceController
类添加功能看似简单。遗憾的是,该类将其所有内部逻辑隐藏在 private
方法和属性中,因此我们无法直接使用它们并与服务和 SCM 实例句柄进行交互。
然而,为了实现我的目标,我使用了三种要素:
- 一个好的反编译器:JetBrains dotPeek
- 反射框架
- 一位同事的一些提示,用户aejw,文章 Map Network Drive (API) 的作者
利用它们,我能够为我的扩展类添加我想要的功能,同时还遵守了 Microsoft 的一致性检查。
关于代码
项目结构非常简单。它分为四个类:
DllNames
:它是 Windows DLL 名称的容器,其他类使用它来进行一些 Win32 API 函数的 P-Invoke 调用。NetworkConnectionHelper
:它是 WNet API(WNetAddConnection2A
,WNetCancelConnection2A
)的包装器,并添加了一些ExtendedServiceController
类使用的高级public
函数。WindowsServicesHelper
:它是 Microsoft Windows Services API(ChangeServiceConfig
,QueryServiceConfig
,CloseServiceHandle
)的包装器,供ExtendedServiceController
类使用。
最后,您唯一需要处理的类是:ExtendedServiceController
。现在,我们将了解如何扩展标准的 ServiceController 类,以及如何解决私有继承问题。
如何获取/设置服务启动模式
如果您熟悉任何 Win32 平台的原生语言,更改 Windows 服务的启动模式并非易事。我们选择 C++ 作为示例,因此更改服务启动模式的任务可以通过以下几行代码完成:
BOOL ChangeServiceStartupMode(
LPCTSTR lpszMachineName,
LPCTSTR lpszServiceName,
DWORD dwStartupMode
)
{
SC_HANDLE hSCM;
SC_HANDLE hService;
hSCM = OpenSCManager(lpszMachineName, NULL, SC_MANAGER_ALL_ACCESS);
if (NULL == hSCM)
{
return false;
}
hService = OpenService(hSCM, lpszServiceName, SERVICE_CHANGE_CONFIG);
if (hService == NULL)
{
CloseServiceHandle(hSCM);
return false;
}
if (!ChangeServiceConfig(
hService,
SERVICE_NO_CHANGE,
dwStartupMode,
SERVICE_NO_CHANGE,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL
))
{
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);
return false;
}
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);
return true;
}
参数 dwStartType
可以取以下值之一:
SERVICE_AUTO_START 0x00000002
SERVICE_BOOT_START 0x00000000
SERVICE_DEMAND_START 0x00000003
SERVICE_DISABLED 0x00000004
SERVICE_SYSTEM_START 0x00000001
其对应的是启动模式查询过程。
BOOL QueryServiceStartupMode(
LPCTSTR lpszMachineName,
LPCTSTR lpszServiceName,
DWORD& dwStartupMode
)
{
SC_HANDLE hSCM;
SC_HANDLE hService;
LPQUERY_SERVICE_CONFIG lpServiceConfig;
DWORD dwBytesNeeded;
DWORD cbBufSize;
DWORD dwError;
hSCM = OpenSCManager(lpszMachineName, NULL, SC_MANAGER_ALL_ACCESS);
if (NULL == hSCM)
{
return false;
}
hService = OpenService(hSCM, lpszServiceName, SERVICE_QUERY_CONFIG);
if (hService == NULL)
{
CloseServiceHandle(hSCM);
return false;
}
if( !QueryServiceConfig(hService, NULL, 0, &dwBytesNeeded))
{
dwError = GetLastError();
if (ERROR_INSUFFICIENT_BUFFER == dwError)
{
cbBufSize = dwBytesNeeded;
lpServiceConfig = (LPQUERY_SERVICE_CONFIG) LocalAlloc(LMEM_FIXED, cbBufSize);
}
else
{
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);
return false;
}
}
if (!QueryServiceConfig(hService, lpServiceConfig, cbBufSize, &dwBytesNeeded))
{
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);
return false;
}
dwStartupMode = lpServiceConfig->dwStartType;
LocalFree(lpServiceConfig);
return true;
}
现在我们需要将上述代码翻译成 C# 并将其集成到继承自 ServiceController
类的 ExtendedServiceController
中。
不幸的是,这项任务并非易事,因为 ServiceController
类本身管理着 SCM 数据库和服务句柄,而且它们是私有的,因此对任何继承的类成员都是隐藏的。
为了解决这个问题,我不得不反编译 ServiceController
类,并利用反射框架服务来复制某些属性和方法的行为。
为了发布方法和属性来访问祖先的 private
成员,我编写了一些通用的辅助方法。
private FieldInfo GetPrivateFieldInfo(string sFieldName)
{
Type oType = typeof(ServiceController);
FieldInfo oFieldInfo = oType.GetField(
sFieldName,
(BindingFlags.Instance | BindingFlags.NonPublic)
);
return oFieldInfo;
}
private T GetPrivateField<T>(string sFieldName)
{
FieldInfo oFieldInfo = GetPrivateFieldInfo(sFieldName);
Debug.Assert(null != oFieldInfo);
return (T)oFieldInfo.GetValue(this);
}
private MethodInfo GetPrivateMethodInfo(string sMethodName, bool bStatic)
{
Type oType = typeof(ServiceController);
MethodInfo oMethodInfo = oType.GetMethod(
sMethodName,
(BindingFlags.NonPublic | (bStatic ? BindingFlags.Static : BindingFlags.Instance))
);
return oMethodInfo;
}
使用上述辅助方法,我重新发布了一些 ServiceController
类用于控制服务句柄状态的隐藏属性和方法。
以下是我们扩展服务控制器所需的:
private bool BrowseGranted
{
get
{
return GetPrivateField<bool>("browseGranted");
}
set
{
SetPrivateField("browseGranted", value);
}
}
通过上述属性,我们可以检查服务控制器是否已获得浏览(查询)权限。
private bool ControlGranted
{
get
{
return GetPrivateField<bool>("controlGranted");
}
set
{
SetPrivateField("controlGranted", value);
}
}
通过上述属性,我们可以检查服务控制器是否已获得控制(启动/停止/配置)权限。
private Win32Exception CreateSafeWin32Exception()
{
MethodInfo oMethodInfo = GetPrivateMethodInfo("CreateSafeWin32Exception", true);
Debug.Assert(null != oMethodInfo);
return (Win32Exception)oMethodInfo.Invoke(this, null);
}
通过上述方法,ExtendedServiceController
类可以在遵守祖先类强制执行的某些约束的情况下,引发 Win32 异常。它被 ServiceController
类的大多数 private
方法使用。
private IntPtr GetServiceHandle(uint nDesiredAccess)
{
MethodInfo oMethodInfo = GetPrivateMethodInfo("GetServiceHandle", false);
Debug.Assert(null != oMethodInfo);
return (IntPtr)oMethodInfo.Invoke(this, new object[] { (int)nDesiredAccess });
}
上述方法是 ServiceController
类中最重要的 private
方法。给定所需的访问模式(SERVICE_QUERY_CONFIG
或 SERVICE_CHANGE_CONFIG
),通过此方法,我们可以获得一个有效且安全的句柄,以传递给 QueryServiceConfig
和 ChangeServiceConfig
方法。在后台,它调用 Win32 API OpenSCManager
和 OpenService
。
现在我们有了编写 StartMode
属性的访问器方法所需的所有要素。
public ServiceStartMode StartMode
{
get
{
return GetStartMode();
}
set
{
SetStartMode(value);
}
}
获取服务启动模式
这里,我使用了一个辅助方法来检查浏览服务权限。
private void CheckBrowsePermission()
{
if (!BrowseGranted)
{
new ServiceControllerPermission(
ServiceControllerPermissionAccess.Browse,
MachineName,
ServiceName
).Demand();
BrowseGranted = true;
}
}
以及两个状态实例字段:
private ServiceStartMode m_eStartMode;
private bool m_bStartModeAvailable;
如您所见,getter 方法与 C++ 实现非常相似,我们通过 private
(安全)ServiceController
方法获取所需的句柄。
private ServiceStartMode GetStartMode()
{
if (m_bStartModeAvailable)
{
return m_eStartMode;
}
CheckBrowsePermission();
IntPtr oServiceHandle = GetServiceHandle(WindowsServicesHelper.SERVICE_QUERY_CONFIG);
try
{
int nBytesNeeded;
if (!WindowsServicesHelper.QueryServiceConfig(
oServiceHandle,
IntPtr.Zero,
0,
out nBytesNeeded
))
{
int nLastWin32Error = Marshal.GetLastWin32Error();
if (WindowsServicesHelper.ERROR_INSUFFICIENT_BUFFER != nLastWin32Error)
{
throw CreateSafeWin32Exception();
}
IntPtr oConfigPointer = Marshal.AllocHGlobal(nBytesNeeded);
try
{
if (!WindowsServicesHelper.QueryServiceConfig(
oServiceHandle,
oConfigPointer,
nBytesNeeded,
out nBytesNeeded
))
{
throw CreateSafeWin32Exception();
}
WindowsServicesHelper.QUERY_SERVICE_CONFIG oQueryServiceConfig =
new WindowsServicesHelper.QUERY_SERVICE_CONFIG();
Marshal.PtrToStructure(oConfigPointer, oQueryServiceConfig);
m_eStartMode = (ServiceStartMode)oQueryServiceConfig.dwStartType;
m_bStartModeAvailable = true;
}
finally
{
Marshal.FreeHGlobal(oConfigPointer);
}
}
return m_eStartMode;
}
finally
{
WindowsServicesHelper.CloseServiceHandle(oServiceHandle);
}
}
记住,QUERY_SERVICE_CONFIG struct
包含一些不安全成员,因此您必须在编译项目时启用“允许不安全代码”。
设置服务启动模式
这里我也使用了一个辅助方法来检查服务控制权限。
private void CheckControlPermission()
{
if (!ControlGranted)
{
new ServiceControllerPermission(
ServiceControllerPermissionAccess.Control,
MachineName,
ServiceName
).Demand();
ControlGranted = true;
}
}
以下是 setter 方法的实现。
private void SetStartMode(ServiceStartMode eStartMode)
{
if (m_bStartModeAvailable && (eStartMode == m_eStartMode))
{
return;
}
CheckControlPermission();
IntPtr oServiceHandle = GetServiceHandle(
WindowsServicesHelper.SERVICE_QUERY_CONFIG
| WindowsServicesHelper.SERVICE_CHANGE_CONFIG
);
try
{
if (!WindowsServicesHelper.ChangeServiceConfig(
oServiceHandle,
WindowsServicesHelper.SERVICE_NO_CHANGE,
(int)eStartMode,
WindowsServicesHelper.SERVICE_NO_CHANGE,
null,
null,
IntPtr.Zero,
null,
null,
null,
null
))
{
throw CreateSafeWin32Exception();
}
m_eStartMode = eStartMode;
m_bStartModeAvailable = true;
}
finally
{
WindowsServicesHelper.CloseServiceHandle(oServiceHandle);
}
}
同样,对于这个方法,实现与 C++ 对应的方法非常相似。
如何检查服务是否存在
检查服务(本地或远程)是否存在非常简单,我们可以基于祖先的 private
方法 GenerateStatus
编写一个方法。
以下是属性及其 getter 方法的声明。
public bool Exists
{
get
{
return GetExists();
}
}
private void GenerateStatus()
{
MethodInfo oMethodInfo = GetPrivateMethodInfo("GenerateStatus", false);
Debug.Assert(null != oMethodInfo);
oMethodInfo.Invoke(this, null);
}
private bool GetExists()
{
if (m_bExistsAvailable)
{
return m_bExists;
}
try
{
GenerateStatus();
m_bExists = true;
}
catch
{
m_bExists = false;
}
m_bExistsAvailable = true;
return m_bExists;
}
如何管理远程服务
ServiceController
类可以通过其重载构造函数之一来寻址远程服务,将机器名称或 IP 地址传递给它。
在这种情况下,如果我们的应用程序在目标远程计算机上以已验证的上下文运行(计算机位于同一域或共享相同的用户名和密码),并且登录的用户具有访问远程 SCM(服务控制管理器)所需的权限,那么一切都会正常工作。
如果上述一个或多个要求不满足,ServiceController
类的方法调用将会失败!
我们如何解决这个问题?就像我们应该在 Windows 命令行中做的那样。我们应该建立一个到目标计算机的已验证网络会话,然后管理远程服务。
请看以下示例:
C:\> net use /user:<USER-NAME> \\<MACHINE-NAME> <PASSWORD>
C:\> sc.exe \\<MACHINE-NAME> stop <SERVICE-NAME>
C:\> net use \\<MACHINE-NAME> /delete
要使用 C# 并结合本地 Win32 API 完成此任务,我们需要通过 WNetAddConnection2A
函数创建到目标计算机 IPC$ 共享的连接,然后调用服务控制器方法,最后通过 WNetCancelConnection2A
函数撤销连接。
此流程已由以下两个方法封装(非常感谢aejw的文章 Map Network Drive (API))
public bool ConnectedToRemoteMachine
{
get
{
return !string.IsNullOrWhiteSpace(m_sRemoteResource);
}
}
public void ConnectToRemoteMachine(string sUserName, string sPassword)
{
if (!ConnectedToRemoteMachine)
{
NetworkConnectionHelper oNetworkConnectionHelper = new NetworkConnectionHelper();
string sRemoteResource =
string.Format(@"\\{0}\IPC$", MachineName);
if (!oNetworkConnectionHelper.AddConnection(
sRemoteResource,
sUserName,
sPassword
))
{
throw CreateSafeWin32Exception();
}
m_sRemoteResource = sRemoteResource;
}
}
public void DisconnectFromRemoteMachine()
{
if (ConnectedToRemoteMachine)
{
NetworkConnectionHelper oNetworkConnectionHelper = new NetworkConnectionHelper();
bool bResult =
oNetworkConnectionHelper.CancelConnection(m_sRemoteResource);
m_sRemoteResource = null;
if (!bResult)
{
throw CreateSafeWin32Exception();
}
}
}
因此,如果您需要管理远程服务,您应该简单地通过有效且授权的用户连接到目标,然后使用服务控制器。
为了释放网络会话,请记住断开服务控制器的连接。
ExtendedServiceController 实战
最后,我想向您展示一个关于 ExtendedServiceController
类扩展功能的自注释示例。
static void Main() { #region Environment setup const string sMachineName = null; const string sServiceName = "RemoteRegistry"; const string sUserName = "Administrator"; // or "Domain\UserName" const string sPassword = "<PASSWORD>"; Library.ExtendedServiceController oExtendedServiceController = new Library.ExtendedServiceController( sServiceName, (sMachineName ?? ".") ); #endregion try { if (!string.IsNullOrWhiteSpace(sMachineName)) { oExtendedServiceController.ConnectToRemoteMachine(sUserName, sPassword); } if (!oExtendedServiceController.Exists) { return; } if (ServiceControllerStatus.Running == oExtendedServiceController.Status) { oExtendedServiceController.Stop(); } if (ServiceStartMode.Automatic != oExtendedServiceController.StartMode) { oExtendedServiceController.StartMode = ServiceStartMode.Automatic; } oExtendedServiceController.Start(); if (oExtendedServiceController.ConnectedToRemoteMachine) { oExtendedServiceController.DisconnectFromRemoteMachine(); } } catch (Exception oException) { Console.WriteLine(oException.Message); } finally { Console.WriteLine("Hit ENTER to exit..."); Console.ReadLine(); } }
感谢您的关注!
致谢
- JetBrains dotPeek 反编译器
- 平台 PInvoke.net
- 用户aejw,文章 Map Network Drive (API)
历史
- 2013 年 10 月 2 日 - 首次文章发布