美文网首页C#.NET.NET Core
C#/.NET基于Topshelf创建Windows服务的守护程

C#/.NET基于Topshelf创建Windows服务的守护程

作者: Rector | 来源:发表于2019-05-24 15:24 被阅读3次

    本文首发于:码友网--一个专注.NET/.NET Core开发的编程爱好者社区。

    文章目录

    C#/.NET基于Topshelf创建Windows服务的系列文章目录:

    1. C#/.NET基于Topshelf创建Windows服务程序及服务的安装和卸载 (1)
    2. 在C#/.NET应用程序开发中创建一个基于Topshelf的应用程序守护进程(服务) (2)
    3. C#/.NET基于Topshelf创建Windows服务的守护程序作为服务启动的客户端桌面程序不显示UI界面的问题分析和解决方案 (3)

    前言

    在上一篇文章《在C#/.NET应用程序开发中创建一个基于Topshelf的应用程序守护进程(服务)》的最后,我给大家抛出了一个遗留的问题--在将TopshelfDemoService程序作为Windows服务安装的情况下,由它守护并启动的客户端程序是没有UI界面的。到这里,我们得分析为什么会出现这个问题,为什么在桌面应用程序模式下可以显示UI界面,而在服务模式下没有UI界面?

    分析问题(Session 0 隔离)

    通过查阅资料,这是由于Session 0 隔离作用的结果。那么什么又是Session 0 隔离呢?

    在Windows XP、Windows Server 2003 或早期Windows 系统时代,当第一个用户登录系统后服务和应用程序是在同一个Session 中运行的。这就是Session 0 如下图所示:

    image

    但是这种运行方式提高了系统安全风险,因为服务是通过提升了用户权限运行的,而应用程序往往是那些不具备管理员身份的普通用户运行的,其中的危险显而易见。

    从Vista 开始Session 0 中只包含系统服务,其他应用程序则通过分离的Session 运行,将服务与应用程序隔离提高系统的安全性。如下图所示:

    image

    这样使得Session 0 与其他Session 之间无法进行交互,不能通过服务向桌面用户弹出信息窗口、UI 窗口等信息。这也就是为什么刚才我说那个图已经不能通过当前桌面进行截图了。

    image

    潜在的问题

    解决方案

    在了解了Session 0 隔离之后,给出一些有关创建服务程序以及由服务托管的驱动程序的建议:

    1、与应用程序通信时,使用RPC、命名管道等C/S模式代替窗口消息
    2、如果服务程序需要UI与用户交互的话,有两种方式:
    ①用WTSSendMessage来创建一个消息框与用户交互
    ②使用一个代理(agent)来完成跟用户的交互,服务程序通过CreateProcessAsUser创建代理。
    并用RPC或者命名管道等方式跟代理通信,从而完成复杂的界面交互。
    3、应该在用户的Session中查询显示属性,如果在Session 0中做这件事,将会得到不正确的结果。
    4、明确地使用Local或者Global为命名对象命名,Local/为Session/<n>/BaseNamedObject/,Global/为BaseNamedObject/
    5、将程序放在实际环境中测试是最好的方法,如果条件不允许,可以在XP的FUS下测试。在XP的FUS下能工作的服务程序将很可能可以在新版系统中工作,注意XP的FUS下的测试不能检测到在Session 0下跟视频驱动有关的问题

    本文我们的服务程序将通过CreateProcessAsUser创建代理来实现Session 0隔离的穿透。

    在项目[TopshelfDemoService]中创建一个静态扩展帮助类ProcessExtensions.cs,代码如下:

    using System;
    using System.Runtime.InteropServices;
    
    namespace TopshelfDemoService
    {
        /// <summary>
        /// 进程静态扩展类
        /// </summary>
        public static class ProcessExtensions
        {
            #region Win32 Constants
    
            private const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
            private const int CREATE_NO_WINDOW = 0x08000000;
    
            private const int CREATE_NEW_CONSOLE = 0x00000010;
    
            private const uint INVALID_SESSION_ID = 0xFFFFFFFF;
            private static readonly IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;
    
            #endregion
    
            #region DllImports
    
            [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
            private static extern bool CreateProcessAsUser(
                IntPtr hToken,
                String lpApplicationName,
                String lpCommandLine,
                IntPtr lpProcessAttributes,
                IntPtr lpThreadAttributes,
                bool bInheritHandle,
                uint dwCreationFlags,
                IntPtr lpEnvironment,
                String lpCurrentDirectory,
                ref STARTUPINFO lpStartupInfo,
                out PROCESS_INFORMATION lpProcessInformation);
    
            [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")]
            private static extern bool DuplicateTokenEx(
                IntPtr ExistingTokenHandle,
                uint dwDesiredAccess,
                IntPtr lpThreadAttributes,
                int TokenType,
                int ImpersonationLevel,
                ref IntPtr DuplicateTokenHandle);
    
            [DllImport("userenv.dll", SetLastError = true)]
            private static extern bool CreateEnvironmentBlock(ref IntPtr lpEnvironment, IntPtr hToken, bool bInherit);
    
            [DllImport("userenv.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            private static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);
    
            [DllImport("kernel32.dll", SetLastError = true)]
            private static extern bool CloseHandle(IntPtr hSnapshot);
    
            [DllImport("kernel32.dll")]
            private static extern uint WTSGetActiveConsoleSessionId();
    
            [DllImport("Wtsapi32.dll")]
            private static extern uint WTSQueryUserToken(uint SessionId, ref IntPtr phToken);
    
            [DllImport("wtsapi32.dll", SetLastError = true)]
            private static extern int WTSEnumerateSessions(
                IntPtr hServer,
                int Reserved,
                int Version,
                ref IntPtr ppSessionInfo,
                ref int pCount);
    
            #endregion
    
            #region Win32 Structs
    
            private enum SW
            {
                SW_HIDE = 0,
                SW_SHOWNORMAL = 1,
                SW_NORMAL = 1,
                SW_SHOWMINIMIZED = 2,
                SW_SHOWMAXIMIZED = 3,
                SW_MAXIMIZE = 3,
                SW_SHOWNOACTIVATE = 4,
                SW_SHOW = 5,
                SW_MINIMIZE = 6,
                SW_SHOWMINNOACTIVE = 7,
                SW_SHOWNA = 8,
                SW_RESTORE = 9,
                SW_SHOWDEFAULT = 10,
                SW_MAX = 10
            }
    
            private enum WTS_CONNECTSTATE_CLASS
            {
                WTSActive,
                WTSConnected,
                WTSConnectQuery,
                WTSShadow,
                WTSDisconnected,
                WTSIdle,
                WTSListen,
                WTSReset,
                WTSDown,
                WTSInit
            }
    
            [StructLayout(LayoutKind.Sequential)]
            private struct PROCESS_INFORMATION
            {
                public IntPtr hProcess;
                public IntPtr hThread;
                public uint dwProcessId;
                public uint dwThreadId;
            }
    
            private enum SECURITY_IMPERSONATION_LEVEL
            {
                SecurityAnonymous = 0,
                SecurityIdentification = 1,
                SecurityImpersonation = 2,
                SecurityDelegation = 3,
            }
    
            [StructLayout(LayoutKind.Sequential)]
            private struct STARTUPINFO
            {
                public int cb;
                public String lpReserved;
                public String lpDesktop;
                public String lpTitle;
                public uint dwX;
                public uint dwY;
                public uint dwXSize;
                public uint dwYSize;
                public uint dwXCountChars;
                public uint dwYCountChars;
                public uint dwFillAttribute;
                public uint dwFlags;
                public short wShowWindow;
                public short cbReserved2;
                public IntPtr lpReserved2;
                public IntPtr hStdInput;
                public IntPtr hStdOutput;
                public IntPtr hStdError;
            }
    
            private enum TOKEN_TYPE
            {
                TokenPrimary = 1,
                TokenImpersonation = 2
            }
    
            [StructLayout(LayoutKind.Sequential)]
            private struct WTS_SESSION_INFO
            {
                public readonly UInt32 SessionID;
    
                [MarshalAs(UnmanagedType.LPStr)]
                public readonly String pWinStationName;
    
                public readonly WTS_CONNECTSTATE_CLASS State;
            }
    
            #endregion
    
            // Gets the user token from the currently active session
            private static bool GetSessionUserToken(ref IntPtr phUserToken)
            {
                var bResult = false;
                var hImpersonationToken = IntPtr.Zero;
                var activeSessionId = INVALID_SESSION_ID;
                var pSessionInfo = IntPtr.Zero;
                var sessionCount = 0;
    
                // Get a handle to the user access token for the current active session.
                if (WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, ref pSessionInfo, ref sessionCount) != 0)
                {
                    var arrayElementSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO));
                    var current = pSessionInfo;
    
                    for (var i = 0; i < sessionCount; i++)
                    {
                        var si = (WTS_SESSION_INFO)Marshal.PtrToStructure(current, typeof(WTS_SESSION_INFO));
                        current += arrayElementSize;
    
                        if (si.State == WTS_CONNECTSTATE_CLASS.WTSActive)
                        {
                            activeSessionId = si.SessionID;
                        }
                    }
                }
    
                // If enumerating did not work, fall back to the old method
                if (activeSessionId == INVALID_SESSION_ID)
                {
                    activeSessionId = WTSGetActiveConsoleSessionId();
                }
    
                if (WTSQueryUserToken(activeSessionId, ref hImpersonationToken) != 0)
                {
                    // Convert the impersonation token to a primary token
                    bResult = DuplicateTokenEx(hImpersonationToken, 0, IntPtr.Zero,
                        (int)SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, (int)TOKEN_TYPE.TokenPrimary,
                        ref phUserToken);
    
                    CloseHandle(hImpersonationToken);
                }
    
                return bResult;
            }
    
            public static bool StartProcessAsCurrentUser(string appPath, string cmdLine = null, string workDir = null, bool visible = true)
            {
                var hUserToken = IntPtr.Zero;
                var startInfo = new STARTUPINFO();
                var procInfo = new PROCESS_INFORMATION();
                var pEnv = IntPtr.Zero;
                int iResultOfCreateProcessAsUser;
    
                startInfo.cb = Marshal.SizeOf(typeof(STARTUPINFO));
    
                try
                {
                    if (!GetSessionUserToken(ref hUserToken))
                    {
                        throw new Exception("StartProcessAsCurrentUser: GetSessionUserToken failed.");
                    }
    
                    uint dwCreationFlags = CREATE_UNICODE_ENVIRONMENT | (uint)(visible ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW);
                    startInfo.wShowWindow = (short)(visible ? SW.SW_SHOW : SW.SW_HIDE);
                    startInfo.lpDesktop = "winsta0\\default";
    
                    if (!CreateEnvironmentBlock(ref pEnv, hUserToken, false))
                    {
                        throw new Exception("StartProcessAsCurrentUser: CreateEnvironmentBlock failed.");
                    }
    
                    if (!CreateProcessAsUser(hUserToken,
                        appPath, // Application Name
                        cmdLine, // Command Line
                        IntPtr.Zero,
                        IntPtr.Zero,
                        false,
                        dwCreationFlags,
                        pEnv,
                        workDir, // Working directory
                        ref startInfo,
                        out procInfo))
                    {
                        iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error();
                        throw new Exception("StartProcessAsCurrentUser: CreateProcessAsUser failed.  Error Code -" + iResultOfCreateProcessAsUser);
                    }
    
                    iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error();
                }
                finally
                {
                    CloseHandle(hUserToken);
                    if (pEnv != IntPtr.Zero)
                    {
                        DestroyEnvironmentBlock(pEnv);
                    }
                    CloseHandle(procInfo.hThread);
                    CloseHandle(procInfo.hProcess);
                }
    
                return true;
            }
    
        }
    }
    

    修改ProcessHelper.cs为如下代码:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    
    namespace TopshelfDemoService
    {
        /// <summary>
        /// 进程处理帮助类
        /// </summary>
        internal class ProcessorHelper
        {
            /// <summary>
            /// 获取当前计算机所有的进程列表(集合)
            /// </summary>
            /// <returns></returns>
            public static List<Process> GetProcessList()
            {
                return GetProcesses().ToList();
            }
    
            /// <summary>
            /// 获取当前计算机所有的进程列表(数组)
            /// </summary>
            /// <returns></returns>
            public static Process[] GetProcesses()
            {
                var processList = Process.GetProcesses();
                return processList;
            }
    
            /// <summary>
            /// 判断指定的进程是否存在
            /// </summary>
            /// <param name="processName"></param>
            /// <returns></returns>
            public static bool IsProcessExists(string processName)
            {
                return Process.GetProcessesByName(processName).Length > 0;
            }
    
            /// <summary>
            /// 启动一个指定路径的应用程序
            /// </summary>
            /// <param name="applicationPath"></param>
            /// <param name="args"></param>
            public static void RunProcess(string applicationPath, string args = "")
            {
                try
                {
                    ProcessExtensions.StartProcessAsCurrentUser(applicationPath, args);
                }
                catch (Exception e)
                {
                    var psi = new ProcessStartInfo
                    {
                        FileName = applicationPath,
                        WindowStyle = ProcessWindowStyle.Normal,
                        Arguments = args
                    };
                    Process.Start(psi);
                }
            }
        }
    }
    

    其中更改了方法RunProcess()的调用方式。

    重新编译服务程序项目[TopshelfDemoService],并将它作为Windows服务安装,最后启动服务。守护进程服务将启动一个带UI界面的客户端程序。大功告成!!!

    我是Rector,希望本文的关于Topshelf服务和守护程序设计对需要的朋友有所帮助。

    感谢花你宝贵的时间阅读!!!

    参考资料

    穿透Session 0 隔离(一)
    Windows中Session 0隔离对服务程序和驱动程序的影响
    CreateProcessAsUser

    源代码下载

    本示例代码托管地址可以在原出处找到:示例代码下载地址

    相关文章

      网友评论

        本文标题:C#/.NET基于Topshelf创建Windows服务的守护程

        本文链接:https://www.haomeiwen.com/subject/oqvbzqtx.html