c# 通过Windows服务启动Winform程序

背景

确保服务器上的一个软件保持运行状态,即使服务器断电重启后,软件也能随之自动启动。

开发过程中的问题

  1. 服务运行后不显示程序界面,但进程中可以看到程序;(已解决)
  2. 在win10、win11系统下,可通过服务启动程序并显示UI界面,但在windows server2019系统下安装服务,启动不了程序,甚至进程中都看不到程序。(已解决)

分析

在xp以后,windows系统引入了session 0 隔离机制,window service独立运行在session0下,依次给后续的登录用户分配sessionX(X =1,2,3…),session0没有权限运行UI。所以在window xp以后的系统下,window service调用有UI的application时只能看到程序进程但不能运行程序的UI。

解决思路

通过调用api函数CreateProcessAsUser在用户会话中创建新进程,新进程负责调用应用程序的UI。

详细步骤

1.创建服务(https://www.cnblogs.com/yinrq/p/5587464.html)
2.代码,具体如下(https://zhuanlan.zhihu.com/p/414479644)
WinAPI类

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace ProcessGuard.Common.Utility
{
    public class WinAPI
    {
        #region Structures
        [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;
        }
        [StructLayout(LayoutKind.Sequential)]
        public struct PROCESS_INFORMATION
        {
            public IntPtr hProcess;
            public IntPtr hThread;
            public uint dwProcessId;
            public uint dwThreadId;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct WTS_SESSION_INFO
        {
            public readonly uint SessionID;

            [MarshalAs(UnmanagedType.LPStr)]
            public readonly string pWinStationName;

            public readonly WTS_CONNECTSTATE_CLASS State;
        }

        #endregion

        #region Enumerations
        private enum TOKEN_TYPE : int
        {
            TokenPrimary = 1,
            TokenImpersonation = 2
        }
        private enum SECURITY_IMPERSONATION_LEVEL : int
        {
            SecurityAnonymous = 0,
            SecurityIdentification = 1,
            SecurityImpersonation = 2,
            SecurityDelegation = 3,
        }
        private enum SW : int
        {
            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_FORCEMINIMIZE = 11,
        }

        private enum WTS_CONNECTSTATE_CLASS
        {
            WTSActive,
            WTSConnected,
            WTSConnectQuery,
            WTSShadow,
            WTSDisconnected,
            WTSIdle,
            WTSListen,
            WTSReset,
            WTSDown,
            WTSInit
        }

        #endregion

        #region Constants
        private const uint MAXIMUM_ALLOWED = 0x2000000;
        private const int CREATE_NEW_CONSOLE = 0x00000010;
        private const int CREATE_NO_WINDOW = 0x08000000;
        private const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
        private const int NORMAL_PRIORITY_CLASS = 0x20;
        private const int STARTF_USESHOWWINDOW = 0x00000001;
        #endregion

        #region Win32 API Imports
        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool CloseHandle(IntPtr hSnapshot);
        [DllImport("kernel32.dll")]
        private static extern uint WTSGetActiveConsoleSessionId();
        [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
        private extern static bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes,
            IntPtr lpThreadAttributes, bool bInheritHandle, int dwCreationFlags, IntPtr lpEnvironment,
            string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
        [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")]
        private extern static 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("wtsapi32.dll", SetLastError = true)]
        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);

        [DllImport("wtsapi32.dll", SetLastError = false)]
        public static extern void WTSFreeMemory(IntPtr memory);

        #endregion

        /// <summary>
        /// 在Seesion 0,主要是为了在Windows Service中启动带有交互式界面的程序
        /// </summary>
        /// <param name="applicationFullPath">要启动的应用程序的完全路径</param>
        /// <param name="startingDir">程序启动时的工作目录,通常传递要启动的程序所在目录即可,特殊情况包括需要在指定的文件夹打开cmd窗口等</param>
        /// <param name="procInfo">创建完成的进程信息</param>
        /// <param name="minimize">是否最小化窗体</param>
        /// <param name="commandLine">表示要使用的命令内容,比如需要启动一个cmd程序,因为获取其真实路径比较麻烦,此时可以直接传"cmd",要启动的应用程序路径留空即可</param>
        /// <returns>创建完成的进程信息</returns>
        public static bool StartProcessInSession0(string applicationFullPath, string startingDir, out PROCESS_INFORMATION procInfo, bool minimize = false, string commandLine = null, bool noWindow = false)
        {
            IntPtr hUserTokenDup = IntPtr.Zero;
            IntPtr hPToken = IntPtr.Zero;
            IntPtr pEnv = IntPtr.Zero;
            procInfo = new PROCESS_INFORMATION();
            bool result = false;
            try
            {
                procInfo = new PROCESS_INFORMATION();

                // 获取当前正在使用的系统用户的session id,每一个登录到系统的用户都有一个唯一的session id
                // 使用两种方法获取当前正在使用的系统用户的session id,每一个登录到系统的用户都有一个唯一的session id
                // 这一步是为了可以正确在当前登录的用户界面启动程序
                uint dwSessionId = WTSGetActiveConsoleSessionId();

                if (WTSQueryUserToken(dwSessionId, ref hPToken) == 0)
                    if (WTSQueryUserToken(WTSGetActiveConsoleSessionId(), ref hPToken) == 0 &&
                        WTSQueryUserToken(GetSessionIdFromEnumerateSessions(), ref hPToken) == 0)
                    {
                        return false;
                    }
                // 复制当前用户的访问令牌,产生一个新令牌
                if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, IntPtr.Zero, (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, (int)TOKEN_TYPE.TokenPrimary, ref hUserTokenDup))
                {
                    return false;
                }
                // lpDesktop参数是用来设置程序启动的界面,这里设置的参数是winsta0\default,代表交互式用户的默认桌面
                STARTUPINFO si = new STARTUPINFO();
                si.cb = (int)Marshal.SizeOf(si);
                si.lpDesktop = @"winsta0\default";
                if (minimize)
                {
                    si.dwFlags = STARTF_USESHOWWINDOW;
                    si.wShowWindow = (short)SW.SW_MINIMIZE;
                }
                // 指定进程的优先级和创建方法,这里代表是普通优先级,并且创建方法是带有UI的进程
                int dwCreationFlags = CREATE_UNICODE_ENVIRONMENT | NORMAL_PRIORITY_CLASS | (noWindow ? CREATE_NO_WINDOW : CREATE_NEW_CONSOLE);
                // 创建新进程的环境变量
                if (!CreateEnvironmentBlock(ref pEnv, hUserTokenDup, false))
                {
                    return false;
                }
                // 在当前用户的session中创建一个新进程
                result = CreateProcessAsUser(hUserTokenDup,        // 用户的访问令牌
                                               applicationFullPath,    // 要执行的程序的路径
                                               commandLine,            // 命令行内容
                                               IntPtr.Zero,            // 设置进程的SECURITY_ATTRIBUTES,主要用来控制对象的访问权限,这里传空值
                                               IntPtr.Zero,            // 设置线程的SECURITY_ATTRIBUTES,控制对象的访问权限,这里传空值
                                               false,                  // 开启的进程不需继承句柄
                                               dwCreationFlags,        // 创建标识
                                               pEnv,                   // 新的环境变量
                                               startingDir,            // 程序启动时的工作目录,通常传递要启动的程序所在目录即可 
                                               ref si,                 // 启动信息
                                               out procInfo            // 用于接收新创建的进程的信息
                                               );
                if (!result)
                {
                    Debug.WriteLine(Marshal.GetLastWin32Error());
                }
            }
            finally
            {
                // 关闭句柄
                CloseHandle(hPToken);
                CloseHandle(hUserTokenDup);
                if (pEnv != IntPtr.Zero)
                {
                    DestroyEnvironmentBlock(pEnv);
                }
                CloseHandle(procInfo.hThread);
                CloseHandle(procInfo.hProcess);
            }
            return result;
        }

        /// <summary>
        /// Get session id via WTSEnumerateSessions
        /// </summary>
        /// <returns></returns>
        private static uint GetSessionIdFromEnumerateSessions()
        {
            var pSessionInfo = IntPtr.Zero;
            try
            {
                var sessionCount = 0;

                // Get a handle to the user access token for the current active session.
                if (WTSEnumerateSessions(IntPtr.Zero, 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)
                        {
                            return si.SessionID;
                        }
                    }
                }

                return uint.MaxValue;
            }
            finally
            {
                WTSFreeMemory(pSessionInfo);
                CloseHandle(pSessionInfo);
            }
        }
       }
      }

Service1.cs

 protected override void OnStart(string[] args)
        {
            System.Timers.Timer timer;
            timer = new System.Timers.Timer();
            timer.Interval = 1000;//设置计时器事件间隔执行时间
            timer.Elapsed += new System.Timers.ElapsedEventHandler(circulation);
            timer.Enabled = true;
        }
private void circulation(object sender, System.Timers.ElapsedEventArgs e)
        {

            string appName = "SocketTool";
            bool runFlag = false;

            Process[] myProcesses = Process.GetProcesses();
            foreach (Process myProcess in myProcesses)
            {
                if (myProcess.ProcessName.CompareTo(appName) == 0)
                {
                    runFlag = true;
                }
            }

            if (!runFlag)   //如果程序没有启动
            {
                WinAPI.PROCESS_INFORMATION procInfo;
                WinAPI.StartProcessInSession0(@"D:\SocketTool.exe", @"D:\", out procInfo,false,null,false );
            }
            else if (runFlag)   //如果程序已经启动
            {

            }
        }

3.生成,拷贝InstallUtil.exe至WIndowsService\bin\Debug目录下
4.以管理员身份打开cmd,安装服务。
安装:Installutil xxx.exe
卸载:Installutil /u xxx.exe

小白记录一下日常,第一次接触WIndows服务,通过网上大神们给的资料,最终也确实解决了问题。
[1]: https://www.cnblogs.com/buli93/p/7086440.html
[2]: https://zhuanlan.zhihu.com/p/414479644
[3]: http://www.codeproject.com/Articles/35773/Subverting-Vista-UAC-in-Both-32-and-64-bit-Archite
[4]: https://www.cnblogs.com/yinrq/p/5587464.html

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值