Windows服务弹出Winform窗体应用程序实现桌面交互

Win服务不能弹窗原因
我们开发的Windows服务程序,默认是不能弹出窗体与用户交互的,服务启动后不能看到窗体。
解决方案
1. 用CreateProcessAsUser这个方法来创建,如果想通过服务向桌面用户Session 创建一个复杂UI 程序界面,则需要使用CreateProcessAsUser 函数为用户创建一个新进程用来运行相应的程序。
2. 允许服务与桌面交互
完整代码:
public static IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;
public static void ShowMessageBox(string message, string title)
{
    int resp = 0;
    WTSSendMessage(
        WTS_CURRENT_SERVER_HANDLE, 
        WTSGetActiveConsoleSessionId(),
        title, title.Length, 
        message, message.Length, 
        0, 0, out resp, false);
}
[DllImport("kernel32.dll", SetLastError = true)]
public static extern int WTSGetActiveConsoleSessionId();
[DllImport("wtsapi32.dll", SetLastError = true)]
public static extern bool WTSSendMessage(
    IntPtr hServer,
    int SessionId,
    String pTitle,
    int TitleLength,
    String pMessage,
    int MessageLength,
    int Style,
    int Timeout,
    out int pResponse,
    bool bWait);
public static void CreateProcess(string app, string path)
{
    bool result;
    IntPtr hToken = WindowsIdentity.GetCurrent().Token;
    IntPtr hDupedToken = IntPtr.Zero;
    PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
    SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
    sa.Length = Marshal.SizeOf(sa);
    STARTUPINFO si = new STARTUPINFO();
    si.cb = Marshal.SizeOf(si);
    int dwSessionID = WTSGetActiveConsoleSessionId();
    result = WTSQueryUserToken(dwSessionID, out hToken);
    
    if (!result)
    {
        ShowMessageBox("WTSQueryUserToken failed", "AlertService Message");
    }
    result = DuplicateTokenEx(
          hToken,
          GENERIC_ALL_ACCESS,
          ref sa,
          (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
          (int)TOKEN_TYPE.TokenPrimary,
          ref hDupedToken
       );
    if (!result)
    {
        ShowMessageBox("DuplicateTokenEx failed" ,"AlertService Message");
    }
    IntPtr lpEnvironment = IntPtr.Zero;
    result = CreateEnvironmentBlock(out lpEnvironment, hDupedToken, false);
    if (!result)
    {
        ShowMessageBox("CreateEnvironmentBlock failed", "AlertService Message");
    }
    result = CreateProcessAsUser(
                         hDupedToken,
                         app,
                         String.Empty,
                         ref sa, ref sa,
                         false, 0, IntPtr.Zero,
                         path, ref si, ref pi);
    if (!result)
    {
        int error = Marshal.GetLastWin32Error();
        string message = String.Format("CreateProcessAsUser Error: {0}", error);
        ShowMessageBox(message, "AlertService Message");
    }
    if (pi.hProcess != IntPtr.Zero)
        CloseHandle(pi.hProcess);
    if (pi.hThread != IntPtr.Zero)
        CloseHandle(pi.hThread);
    if (hDupedToken != IntPtr.Zero)
        CloseHandle(hDupedToken);
}
[StructLayout(LayoutKind.Sequential)]
public struct STARTUPINFO
{
    public Int32 cb;
    public string lpReserved;
    public string lpDesktop;
    public string lpTitle;
    public Int32 dwX;
    public Int32 dwY;
    public Int32 dwXSize;
    public Int32 dwXCountChars;
    public Int32 dwYCountChars;
    public Int32 dwFillAttribute;
    public Int32 dwFlags;
    public Int16 wShowWindow;
    public Int16 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 Int32 dwProcessID;
    public Int32 dwThreadID;
}
[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
    public Int32 Length;
    public IntPtr lpSecurityDescriptor;
    public bool bInheritHandle;
}
public enum SECURITY_IMPERSONATION_LEVEL
{
    SecurityAnonymous,
    SecurityIdentification,
    SecurityImpersonation,
    SecurityDelegation
}
public enum TOKEN_TYPE
{
    TokenPrimary = 1,
    TokenImpersonation
}
public const int GENERIC_ALL_ACCESS = 0x10000000;
[DllImport("kernel32.dll", SetLastError = true,
    CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern bool CloseHandle(IntPtr handle);
[DllImport("advapi32.dll", SetLastError = true,
    CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public static extern bool CreateProcessAsUser(
    IntPtr hToken,
    string lpApplicationName,
    string lpCommandLine,
    ref SECURITY_ATTRIBUTES lpProcessAttributes,
    ref SECURITY_ATTRIBUTES lpThreadAttributes,
    bool bInheritHandle,
    Int32 dwCreationFlags,
    IntPtr lpEnvrionment,
    string lpCurrentDirectory,
    ref STARTUPINFO lpStartupInfo,
    ref PROCESS_INFORMATION lpProcessInformation);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool DuplicateTokenEx(
    IntPtr hExistingToken,
    Int32 dwDesiredAccess,
    ref SECURITY_ATTRIBUTES lpThreadAttributes,
    Int32 ImpersonationLevel,
    Int32 dwTokenType,
    ref IntPtr phNewToken);
[DllImport("wtsapi32.dll", SetLastError=true)]
public static extern bool WTSQueryUserToken(
    Int32 sessionId, 
    out IntPtr Token);
[DllImport("userenv.dll", SetLastError = true)]
static extern bool CreateEnvironmentBlock(
    out IntPtr lpEnvironment, 
    IntPtr hToken, 
    bool bInherit);
安装服务,设置参数:在安装好的服务属性中,登陆项下面的允许服务与桌面交互选中。

到此,就可以弹出cmd程序了。
但是启动自己的程序或其他程序时,会报 CreateProcessAsUser Error: 2 的错误。
需要启动程序的路径改下
result = CreateProcessAsUser(
                                hDupedToken,
                                app,
                                String.Empty,
                                ref sa, ref sa,
                                false, 0, IntPtr.Zero,
                                path, ref si, ref pi);
result = CreateProcessAsUser(
                                hDupedToken,
                                path + app,
                                String.Empty,
                                ref sa, ref sa,
                                false, 0, IntPtr.Zero,
                                path, ref si, ref pi);
CreateProcessAsUser 函数
BOOL WINAPI CreateProcessAsUser(
_In_opt_    HANDLE                hToken,
_In_opt_    LPCTSTR               lpApplicationName,
_Inout_opt_ LPTSTR                lpCommandLine,
_In_opt_    LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_    LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_        BOOL                  bInheritHandles,
_In_        DWORD                 dwCreationFlags,
_In_opt_    LPVOID                lpEnvironment,
_In_opt_    LPCTSTR               lpCurrentDirectory,
_In_        LPSTARTUPINFO         lpStartupInfo,
_Out_       LPPROCESS_INFORMATION lpProcessInformation
);参数说明:
hToken
lpApplicationName
lpCommandLine
lpCurrentDirectory
lpProcessInformation
第一个hToken,是跟用户相关的访问令牌。 相当于认证一样。
第二个参数lpApplicationName, 指的是你所创建进程文件的所在路径包含文件名(最好带着扩展名)
这里需要注意, 当路径中包含空格,如C:\Program Files\MyProcess\App.exe 此时会按以下顺序解析
c:\program.exe Files\MyProcess\App.exec:\program files\MyProcess\App.exe也就是说以 空格 会为分隔符(token)去创建进程(默认扩展名为exe),所以才会有 c:\program.exe。 如果这个目录下恰好有一个名为program.exe文件,那么创建出来的进程就不是你想要的啦。所以当你的进程文件所在路径有空格时最好用“”引用起来。如"C:\Program Files\MyProcess\App.exe".注意别把参数也放到引号中去。或者用windows api 把路径转换为短路径(API名字百度一下就有了)。
第三个参数lpCommandLine,这里可以放你给被创建进程传递的参数。 注意 C程序在传递参数时需要在参数前加一个空格,因为c程序main函数入参默认第一个参数是模块名称,从第二个参数开始才是你想要传递进去的参数,而参数以空格分隔 所以你要加一个空格啦。(其实lpApplicationName为NULL时也可以用lpCommandLine来指定模块文件的路径和模块名称,具体参考MSDN)。
有些进程主函数入参中就有 lpCommandLine参数,直接可以拿来用。有些进程需要调用GetCommandLine函数来获取传入的参数。 具体情况具体对待。
第九个参数lpCurrentDirectory,指的是进程运行的目录(注意不是进程文件所在的目录),为NULL时默认随父进程。
第十一个参数lpProcessInformation,这个是个出参。 返回被创建进程的信息,包括进程PID等,具体参考LPPROCESS_INFORMATION结构体成员

CSCODE.NET - 开发框架文库 - C/S架构Winform快速开发框架
 
 
 
 



