本文首發於:碼友網--一個專一.NET/.NET Core開發的編程愛好者社區。html
C#/.NET基於Topshelf建立Windows服務的系列文章目錄:git
在上一篇文章《在C#/.NET應用程序開發中建立一個基於Topshelf的應用程序守護進程(服務)》的最後,我給你們拋出了一個遺留的問題--在將TopshelfDemoService
程序做爲Windows服務安裝的狀況下,由它守護並啓動的客戶端程序是沒有UI界面的。到這裏,咱們得分析爲何會出現這個問題,爲何在桌面應用程序模式下能夠顯示UI界面,而在服務模式下沒有UI界面?github
經過查閱資料,這是因爲Session 0 隔離做用的結果。那麼什麼又是Session 0 隔離呢?編程
在Windows XP、Windows Server 2003 或早期Windows 系統時代,當第一個用戶登陸系統後服務和應用程序是在同一個Session 中運行的。這就是Session 0 以下圖所示:windows
可是這種運行方式提升了系統安全風險,由於服務是經過提高了用戶權限運行的,而應用程序每每是那些不具有管理員身份的普通用戶運行的,其中的危險顯而易見。api
從Vista 開始Session 0 中只包含系統服務,其餘應用程序則經過分離的Session 運行,將服務與應用程序隔離提升系統的安全性。以下圖所示:數組
這樣使得Session 0 與其餘Session 之間沒法進行交互,不能經過服務向桌面用戶彈出信息窗口、UI 窗口等信息。這也就是爲何剛纔我說那個圖已經不能經過當前桌面進行截圖了。安全
在瞭解了Session 0 隔離以後,給出一些有關建立服務程序以及由服務託管的驅動程序的建議:session
一、與應用程序通訊時,使用RPC、命名管道等C/S模式代替窗口消息
二、若是服務程序須要UI與用戶交互的話,有兩種方式:
①用WTSSendMessage來建立一個消息框與用戶交互
②使用一個代理(agent)來完成跟用戶的交互,服務程序經過CreateProcessAsUser建立代理。
並用RPC或者命名管道等方式跟代理通訊,從而完成複雜的界面交互。
三、應該在用戶的Session中查詢顯示屬性,若是在Session 0中作這件事,將會獲得不正確的結果。
四、明確地使用Local或者Global爲命名對象命名,Local/爲Session//BaseNamedObject/,Global/爲BaseNamedObject/ app
五、將程序放在實際環境中測試是最好的方法,若是條件不容許,能夠在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
本示例代碼託管地址能夠在原出處找到:示例代碼下載地址