65.9K
CodeProject 正在变化。 阅读更多。
Home

扩展的服务控制器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (9投票s)

2013年10月3日

CPOL

5分钟阅读

viewsIcon

31730

downloadIcon

563

.NET ServiceController 类扩展

引言

我想介绍一个 .NET (4.0 或更高版本) ServiceController 类的扩展。它的主要目的是增加一些对日常编程实践有用的功能,但遗憾的是框架本身并未提供这些功能。

ExtendedServiceController 类引入的功能包括:

  • Windows 服务本地和远程启动模式配置
  • Windows 服务本地和远程存在性检查
  • 通过适当的连接和身份验证流程进行 Windows 服务的远程管理

背景

ServiceController 类添加功能看似简单。遗憾的是,该类将其所有内部逻辑隐藏在 private 方法和属性中,因此我们无法直接使用它们并与服务和 SCM 实例句柄进行交互。

然而,为了实现我的目标,我使用了三种要素:

利用它们,我能够为我的扩展类添加我想要的功能,同时还遵守了 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_CONFIGSERVICE_CHANGE_CONFIG),通过此方法,我们可以获得一个有效且安全的句柄,以传递给 QueryServiceConfigChangeServiceConfig 方法。在后台,它调用 Win32 API OpenSCManagerOpenService

现在我们有了编写 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();
    }
}

感谢您的关注!

致谢

历史

  • 2013 年 10 月 2 日 - 首次文章发布
© . All rights reserved.