You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Windows服务启动Windows窗体应用进程存在但界面不显示问题排查

问题原因分析

兄弟,这个问题我之前踩过坑!你看不到窗体的核心原因是Windows服务的会话隔离机制——从Windows Vista开始,系统把服务和用户界面彻底分开了:服务默认运行在「会话0」,这个会话没有用户交互桌面;而我们登录后操作的桌面在「会话1」「会话2」这类用户会话里。你用Process.Start启动的WinForm进程,其实是跟着服务跑在会话0里,所以任务管理器能看到进程,但用户桌面根本看不到界面。

解决方案

针对你的需求(每10分钟弹出窗体询问用户活跃状态),有两个方案供你选择:

方案一:允许服务与桌面交互(不推荐,存在安全风险)

这个方法能让服务直接在用户会话显示界面,但Windows后续版本对这个功能做了严格限制,而且有安全隐患(服务权限高,容易被恶意利用),仅适合测试场景:

  1. 在服务的安装器代码里,把服务账户设置为LocalSystem并开启交互权限:
serviceProcessInstaller1.Account = ServiceAccount.LocalSystem;
serviceProcessInstaller1.Interactive = true;
  1. 安装服务后,在服务属性的「登录」选项卡,勾选「允许服务与桌面交互」。

⚠️ 注意:这个方法在开启UAC的环境下大概率失效,微软已不推荐使用,生产环境别碰!

方案二:在用户会话中启动进程(推荐,符合Windows安全模型)

正确的做法是让服务获取当前活跃用户的会话令牌,然后用CreateProcessAsUserAPI在用户会话中启动WinForm程序,这样窗体就会显示在用户的桌面上。

步骤1:添加Win32 API声明和结构体

在你的服务类里添加这些代码(需要引用System.Runtime.InteropServices):

using System.Runtime.InteropServices;

// Win32 API声明
[DllImport("wtsapi32.dll", SetLastError = true)]
private static extern bool WTSQueryUserToken(uint sessionId, out IntPtr token);

[DllImport("userenv.dll", SetLastError = true)]
private static extern bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);

[DllImport("wtsapi32.dll")]
private static extern bool WTSEnumerateSessions(IntPtr hServer, int Reserved, int Version, ref IntPtr ppSessionInfo, ref int pCount);

[DllImport("wtsapi32.dll")]
private static extern void WTSFreeMemory(IntPtr pMemory);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);

// 所需结构体
[StructLayout(LayoutKind.Sequential)]
private struct STARTUPINFO
{
    public int cb;
    public string lpReserved;
    public string lpDesktop;
    public string lpTitle;
    public int dwX;
    public int dwY;
    public int dwXSize;
    public int dwYSize;
    public int dwXCountChars;
    public int dwYCountChars;
    public int dwFillAttribute;
    public int dwFlags;
    public short wShowWindow;
    public short cbReserved2;
    public IntPtr lpReserved2;
    public IntPtr hStdInput;
    public IntPtr hStdOutput;
    public IntPtr hStdError;
}

[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
{
    public IntPtr hProcess;
    public IntPtr hThread;
    public uint dwProcessId;
    public uint dwThreadId;
}

[StructLayout(LayoutKind.Sequential)]
private struct WTS_SESSION_INFO
{
    public uint SessionId;
    [MarshalAs(UnmanagedType.LPStr)]
    public string pWinStationName;
    public WTS_CONNECTSTATE_CLASS State;
}

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

步骤2:实现获取活跃会话ID和启动进程的方法

添加两个辅助方法:

// 获取当前活跃的用户会话ID
private uint GetActiveSessionId()
{
    IntPtr serverHandle = IntPtr.Zero;
    IntPtr sessionsPtr = IntPtr.Zero;
    int count = 0;

    try
    {
        if (WTSEnumerateSessions(serverHandle, 0, 1, ref sessionsPtr, ref count))
        {
            for (int i = 0; i < count; i++)
            {
                IntPtr currentSession = new IntPtr(sessionsPtr.ToInt64() + i * Marshal.SizeOf(typeof(WTS_SESSION_INFO)));
                WTS_SESSION_INFO sessionInfo = (WTS_SESSION_INFO)Marshal.PtrToStructure(currentSession, typeof(WTS_SESSION_INFO));
                
                // 找到状态为活跃的会话
                if (sessionInfo.State == WTS_CONNECTSTATE_CLASS.WTSActive)
                {
                    return sessionInfo.SessionId;
                }
            }
        }
        return 0;
    }
    finally
    {
        if (sessionsPtr != IntPtr.Zero)
        {
            WTSFreeMemory(sessionsPtr);
        }
        if (serverHandle != IntPtr.Zero)
        {
            CloseHandle(serverHandle);
        }
    }
}

// 在用户会话中启动指定应用
private void LaunchAppInUserSession(string appPath)
{
    uint sessionId = GetActiveSessionId();
    if (sessionId == 0)
    {
        Log.writeEventLog("未找到活跃的用户会话");
        return;
    }

    IntPtr userToken = IntPtr.Zero;
    if (!WTSQueryUserToken(sessionId, out userToken))
    {
        Log.writeEventLog($"获取用户令牌失败:{Marshal.GetLastWin32Error()}");
        return;
    }

    try
    {
        STARTUPINFO si = new STARTUPINFO();
        si.cb = Marshal.SizeOf(si);
        si.lpDesktop = "winsta0\\default"; // 指定用户的默认桌面

        PROCESS_INFORMATION pi = new PROCESS_INFORMATION();

        // 启动进程
        bool success = CreateProcessAsUser(
            userToken,
            appPath,
            null,
            IntPtr.Zero,
            IntPtr.Zero,
            false,
            0,
            IntPtr.Zero,
            Path.GetDirectoryName(appPath),
            ref si,
            out pi
        );

        if (!success)
        {
            Log.writeEventLog($"启动应用失败:{Marshal.GetLastWin32Error()}");
        }
        else
        {
            // 关闭进程和线程句柄,避免资源泄漏
            CloseHandle(pi.hProcess);
            CloseHandle(pi.hThread);
        }
    }
    finally
    {
        if (userToken != IntPtr.Zero)
        {
            CloseHandle(userToken);
        }
    }
}

步骤3:修改服务的Timer触发逻辑

把原来用Process.Start的代码替换成调用LaunchAppInUserSession,同时记得把Timer的间隔改成10分钟(600000毫秒):

protected override void OnStart(string[] args) { 
    tm.Interval = 600000; // 10分钟,替代原来的10秒
    tm.Elapsed += Tm_Elapsed; 
    tm.Start(); 
}

private void Tm_Elapsed(object sender, ElapsedEventArgs e) { 
    if (n == 2) { 
        try { 
            string appPath = @"pathName\appName.exe";
            LaunchAppInUserSession(appPath);
        } catch (Exception exception) { 
            Log.writeEventLog(exception.Message); 
        } 
        var service = new ServiceController("testService"); 
        service.Stop(); 
    } 
    n++; 
}
注意事项
  1. 服务账户需要有足够的权限:推荐使用LocalSystem账户,它默认拥有调用这些API的权限;如果用其他账户,需要给账户分配SeAssignPrimaryTokenPrivilege和SeIncreaseQuotaPrivilege权限。
  2. 确保WinForm程序的目标平台和服务一致(比如都是x64或x86),避免启动失败。
  3. 如果有多个用户登录,这个代码会找到第一个活跃会话的用户,你可以根据需求调整会话选择逻辑。

内容的提问来源于stack exchange,提问作者user10863293

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:15:23