C#/.NET基於Topshelf建立Windows服務的守護程序做爲服務啓動的客戶端桌面程序不顯示UI界面的問題分析和解決方案

本文首發於:碼友網--一個專一.NET/.NET Core開發的編程愛好者社區。html

文章目錄

C#/.NET基於Topshelf建立Windows服務的系列文章目錄:git

  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界面?github

分析問題(Session 0 隔離)

經過查閱資料,這是因爲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/<n>/BaseNamedObject/,Global/爲BaseNamedObject/
五、將程序放在實際環境中測試是最好的方法,若是條件不容許,能夠在XP的FUS下測試。在XP的FUS下能工做的服務程序將極可能能夠在新版系統中工做,注意XP的FUS下的測試不能檢測到在Session 0下跟視頻驅動有關的問題 app

本文咱們的服務程序將經過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

源代碼下載

本示例代碼託管地址能夠在原出處找到:示例代碼下載地址

相關文章
相關標籤/搜索