本文首發於:碼友網--一個專注.NET/.NET Core開發的程式設計愛好者社群。
文章目錄
C#/.NET基於Topshelf建立Windows服務的系列文章目錄:
- C#/.NET基於Topshelf建立Windows服務程式及服務的安裝和解除安裝 (1)
- 在C#/.NET應用程式開發中建立一個基於Topshelf的應用程式守護程式(服務) (2)
- 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 如下圖所示:
但是這種執行方式提高了系統安全風險,因為服務是通過提升了使用者許可權執行的,而應用程式往往是那些不具備管理員身份的普通使用者執行的,其中的危險顯而易見。
從Vista 開始Session 0 中只包含系統服務,其他應用程式則通過分離的Session 執行,將服務與應用程式隔離提高系統的安全性。如下圖所示:
這樣使得Session 0 與其他Session 之間無法進行互動,不能通過服務向桌面使用者彈出資訊視窗、UI 視窗等資訊。這也就是為什麼剛才我說那個圖已經不能通過當前桌面進行截圖了。
潛在的問題
解決方案
在瞭解了Session 0 隔離之後,給出一些有關建立服務程式以及由服務託管的驅動程式的建議:
1、與應用程式通訊時,使用RPC、命名管道等C/S模式代替視窗訊息
2、如果服務程式需要UI與使用者互動的話,有兩種方式:
①用WTSSendMessage來建立一個訊息框與使用者互動
②使用一個代理(agent)來完成跟使用者的互動,服務程式通過CreateProcessAsUser建立代理。
並用RPC或者命名管道等方式跟代理通訊,從而完成複雜的介面互動。
3、應該在使用者的Session中查詢顯示屬性,如果在Session 0中做這件事,將會得到不正確的結果。
4、明確地使用Local或者Global為命名物件命名,Local/為Session//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
原始碼下載
本示例程式碼託管地址可以在原出處找到:示例程式碼下載地址