Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >C#/.NET基于Topshelf创建Windows服务的守护程序不显示UI界面的问题分析和解决方案

C#/.NET基于Topshelf创建Windows服务的守护程序不显示UI界面的问题分析和解决方案

作者头像
Rector
发布于 2019-05-25 08:26:38
发布于 2019-05-25 08:26:38
1.4K00
代码可运行
举报
文章被收录于专栏:.NET开发者社区.NET开发者社区
运行总次数:0
代码可运行

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

C#/.NET基于Topshelf创建Windows服务的守护程序作为服务启动的客户端桌面程序不显示UI界面的问题分析和解决方案

前言

在上一篇文章《在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,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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为如下代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

源代码下载

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-05-24 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
从Win服务启动UI程序
从windows服务启动一个带UI程序的界面,这个需求在xp中是很随意的,从Vista开始似乎没有那么随意了,因为Vista中加入了Session的概念,那么什么是Session,我想这篇文章介绍的应该比我权威的多。Session隔离介绍
用户1175783
2019/09/18
1.1K0
dotnet C# 全局 Windows 鼠标钩子
本文来告诉大家一个简单的方法实现全局的 鼠标钩子 实现封装方法十分简单,请看代码 public class MouseHookEventArgs : EventArgs { public bool Handle { get; set; } /// <inheritdoc /> public MouseHookEventArgs(MouseMessages mouseMessage) { MouseMes
林德熙
2021/12/23
8120
C# 纯控制台创建一个全屏窗口
使用 user32.dll 的 CreateWindowExW 方法就能创建窗口,代码请看
林德熙
2020/08/19
1.1K0
C# Windows服务开发
我要开发一个系统服务,服务的作用是定时检测并关闭其他应用的弹窗,但是开发后却发现,服务在运行是压根获取不到任何窗口。
码客说
2022/05/23
1.3K0
C# Windows服务开发
Windows任务管理 连接用户登录信息 通用类[C#版]
通用类名[ComputerLoginUserInfo.cs] 代码如下: using System; //---引用 using System.Runtime.InteropServices; using System.Text; /// <summary> /// Windows 任务管理器登录用户信息 /// author:Stone_W /// date:2011.1.14 /// </summary> public class ComputerLoginUserInfo { #regio
磊哥
2018/04/26
1.2K0
扫描仪对接(C#)
源代码地址:http://www.codeproject.com/Articles/171666/Twain-for-WPF-Applications-Look-Ma-No-Handles
码客说
2022/09/19
4.8K0
扫描仪对接(C#)
打印自定义纸张大小
长江支流说的办法保留太多了,结果不行,很多类都是他在程序集里自定义的,源码又没公开
Java架构师必看
2021/03/22
7960
WinDbg
WinDbg是一款基于window操作系统的调试工具,它可以帮助我们查出在日常开发工作中可能会遇到的问题;例如:
JusterZhu
2022/12/07
6500
WinDbg
C#中DllImport用法汇总
最近使用DllImport,从网上google后发现,大部分内容都是相同,又从MSDN中搜集下,现将内容汇总,与大家分享。
全栈程序员站长
2022/07/25
2.1K0
wpf键盘记录器
很简单的一个wpf键盘记录器 这个程序我一样用了全局勾子,之前用的都是winform上运行了,前一段时间 在国外的论坛上逛看到了一个wpf能用的就做了一个小程序记录一下,为了方便大家直关的看我在页面上
lpxxn
2018/01/31
1.3K0
wpf键盘记录器
c# 获取cook
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; namespace HQTX_BatchAddData { /// <summary> /// WinInet.dll wrapper /// </summary> internal static class CookieRea
冰封一夏
2019/09/11
7790
.NET Core多线程 (2) 异步 - 上
举个例子,在高峰期去餐厅吃饭,会先排队拿个小票,然后去逛一下玩玩,等到排到时会被通知就餐,这时再回到餐厅就可以点餐了。
Edison Zhou
2023/08/09
3140
.NET Core多线程 (2) 异步 - 上
Unity实战篇 | 使Unity打包的exe程序始终保持屏幕最前端【文末送书】
我这里是用了一个西红柿闹钟的项目用作测试,因为之前使用该闹钟想提高自己的工作效率时发现该闹钟老是容易被其他应用程序遮挡住,所以才想到让他持续运行在屏幕最前端不被盖住,从而督促自己。
呆呆敲代码的小Y
2023/04/09
2.2K0
Unity实战篇 | 使Unity打包的exe程序始终保持屏幕最前端【文末送书】
使用 C# 自动化关闭电脑
我查阅资料发现有一些可使用 C# 关闭用户电脑的方法,但我觉得都不是很简洁,所以我想在这里寻找一种简单或者使用原生 .NET 关闭的方式。
zls365
2021/04/23
5940
C#进阶——记一次USB HID的各种坑(x86,x64,win10,win7)
写工控上位机的搬砖人,难免会遇到USB通讯,在一个项目中,我写的上位机使用USB HID协议和STM32通讯传输数据,从零大概花了几天找例程,找资料,最后是各种搬砖修补,终于出来了一个出版DOME,能和下位机实时通讯了。
vv彭
2022/05/10
2.8K2
系统钩子
设置钩子代码 //定义一个钩子实例 var hookProc = new HookProc(HookProcCallback); //设置钩子 hKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, proc, null, 0); if(hkeyboardHook!=IntPtr.Zero){ //设置成功 }
用户1175783
2019/09/18
9880
使用WINAPI安装Windows服务[转]
using system; using system.runtime.interopservices; namespace myserviceinstaller { class serviceinstaller { #region private variables private string _servicepath; private string _servicename; private string _service
liulun
2022/05/09
5500
C#中通过API实现的打印类---修改自泥人张版本
using System; using System.Collections; using System.Text; using System.Runtime.InteropServices; using System.Security; using System.ComponentModel; using System.Drawing.Printing; namespace PrinterAPI {  public class Printer  {   private Printer()   {   }  ///泥人张版本加强版   #region API声明   [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]    internal struct structPrinterDefaults   {    [MarshalAs(UnmanagedType.LPTStr)]    public String pDatatype;    public IntPtr pDevMode;    [MarshalAs(UnmanagedType.I4)]    public int DesiredAccess;   };   [DllImport("winspool.Drv", EntryPoint = "OpenPrinter", SetLastError = true,     CharSet = CharSet.Unicode, ExactSpelling = false, CallingConvention = CallingConvention.StdCall),   SuppressUnmanagedCodeSecurityAttribute()]   internal static extern bool OpenPrinter([MarshalAs(UnmanagedType.LPTStr)]    string printerName,    out IntPtr phPrinter,    ref structPrinterDefaults pd);   [DllImport("winspool.Drv", EntryPoint = "ClosePrinter", SetLastError = true,     CharSet = CharSet.Unicode, ExactSpelling = false,     CallingConvention = CallingConvention.StdCall), SuppressUnmanagedCodeSecurityAttribute()]   internal static extern bool ClosePrinter(IntPtr phPrinter);   [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]    internal struct structSize   {    public Int32 width;    public Int32 height;   }   [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]    internal struct structRect   {    public Int32 left;    public Int32 top;    public Int32 right;    public Int32 bottom;   }   [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)]    internal struct FormInfo1   {    [FieldOffset(0), MarshalAs(UnmanagedType.I4)]    public uint Flags;    [FieldOffset(4), MarshalAs(UnmanagedType.LPWStr)]    public String pName;    [FieldOffset(8)]    public structSize Size;    [FieldOffset(16)]    public structRect ImageableArea;   };   [StructLayout(LayoutKind.Sequential, CharSet = CharSet
jack.yang
2025/04/05
590
免杀工具Sharperner浅析
Sharperner是一款C#编写的自动化免杀工具,用来生成免杀的exe文件或者C++的loader,在antiscan.me上为全绿,效果可见一斑。
鸿鹄实验室
2021/07/06
1.1K0
免杀工具Sharperner浅析
【c#】控制台程序的显示和隐藏「建议收藏」
在项目开发的过程中,为了方便调试,利用控制台打印出一些提示信息,当安装到现场的时候,我们担心工作人员手误关掉了程序,所以将控制台隐藏,让程序在后台跑。下面是具体的显示和隐藏的代码:
全栈程序员站长
2022/08/10
4.4K0
相关推荐
从Win服务启动UI程序
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档