本文首发于:码友网
<https://codedefault.com/p/create-daemon-service-with-topshelf-in-csharp-application>
--一个专注.NET/.NET Core开发的编程爱好者社区。

文章目录

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

* C#/.NET基于Topshelf创建Windows服务程序及服务的安装和卸载
<https://codedefault.com/p/create-windows-service-with-topshelf-in-csharp-console-application>
(1)
* 在C#/.NET应用程序开发中创建一个基于Topshelf的应用程序守护进程(服务)
<https://codedefault.com/p/create-daemon-service-with-topshelf-in-csharp-application>
(2)
* C#/.NET基于Topshelf创建Windows服务的守护程序作为服务启动的客户端桌面程序不显示UI界面的问题分析和解决方案
<https://codedefault.com/p/launch-a-gui-application-from-a-windows-service-on-windows>
(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 隔离(一)
<http://www.cnblogs.com/gnielee/archive/2010/04/07/session0-isolation-part1.html>
Windows中Session 0隔离对服务程序和驱动程序的影响
<https://blog.csdn.net/wk89665944/article/details/53927028>
CreateProcessAsUser <https://github.com/murrayju/CreateProcessAsUser>

源代码下载

本示例代码托管地址可以在原出处找到:示例代码下载地址
<https://codedefault.com/p/launch-a-gui-application-from-a-windows-service-on-windows>