关于 Quicker 启动进程继承 UIAccess 导致 UIPI 异常的反馈

BUG反馈 · 35 次浏览
暖暖~ 创建于 7小时32分钟前

首先这个问题我目前找到解决方案了. 这里主要是希望Quicker能从官方上同时增加这个解决方案

这个我刚才搜了一下. 遇到的人还不少

使用管理员启动的quicker,通过它启动的其他软件里使用SetParent会有异常 - Quicker

启动wps相关 - Quicker

 

这个是我原来的提问的

运行winform程序,界面异常 - Quicker

表现就是有些软件通过Quicker打开. 界面会错乱

 

这个根本原因是UIAccess有继承. 只要是用Quicker启动的软件. 无论是先用Quicker启动A A再启动B B再启动C C在启动D 都会影响最终的程序受UIAccess. 所以Quicker本身做什么操作目前无法解决这个问题

Quicker启动的都会有UIAccess.如下图

 

这个问题解决方法有两种. 一个是把Quicker移除C盘. 但是这个副作用也大. 更新了又会回到C盘

且移出C盘权限不够了,url也会失效. 这样会导致网页壁纸不能用url链接设置. 原来的自动启动也会失效. 副作用大

 

我这边最终的解决问题是两个:

1.如果是本身自己的软件可以修改代码. 软件可以检测自己是否在UIAccess下. 如果在UIAccess下面. 就重新把自己挂在explorer上

挂在explorer启动应该跟手动启动程序一样, 不会有什么问题.

2.另外做个c#独立exe启动器程序. 这个程序做中间桥梁. 检测如果自己在UIAccess下. 然后自己重新启动把自己挂在explorer上. 

这样这个exe启动器再启动对应的全部程序. 就可以让后面的程序都会在explorer上. 避免受到UIAcess影响

(当然还有第三个解决方案我没有试. 就是把程序加入到任务计划里.直接用任务计划启动. 因为任务计划本身就是系统的. 启动东西不会出问题. 但是我觉得这个方案太复杂了)

不过此问题之前和Ai讨论过. 如果本身Explorer进程不存在. 那么会有运行失败的风险. 不过我觉得Explorer一般很少会不存在,因为都是普通用户.

关了Explorer他们也无法操作桌面了

 

我觉得崔大可以尝试看看能否按照方案2来做一个什么选项或者什么. 能让启动的程序剥离UIAccess, 从Explorer启动. 这样可以使很多软件界面变得正常

 

下面是AI的详解:

问题背景: 当 Quicker 运行在 UIAccess 模式下时,通过 Quicker 启动的子进程(如某些重度依赖窗口管理的 IDE、编辑器)会继承该特权或被系统判定为“辅助功能链路”。对于某些频繁操作 TopMostActivate 和多层级窗口的程序,会触发 Windows 的前台保护机制(Input Suppression)

具体症状:

  1. 子进程窗口显示正常,层级正常。

  2. 输入失效: 鼠标无法点击窗口内部元素(因为被系统层拦截),但系统级快捷键(如 Alt+F4)依然有效。

  3. 根因: UIAccess 改变了 Windows 的前台信任规则。当“UIAccess 链路 + 频繁抢夺前台 + TopMost”同时出现时,Windows 会限制该进程的输入分发。

目前用户的临时解决方案: 开发者必须在子进程中主动检测权限,并利用 ShellExecuteToken 转换,强制通过 Explorer.exe(普通 Medium 权限)重开程序来剥离 UIAccess 标识。

对 Quicker 的建议: 希望 Quicker 官方能在“启动参数”或“运行操作”中增加一个选项:“通过 Explorer 启动(剥离 UIAccess 继承)”

  • 实现逻辑: 调用 IShellDispatch2::ShellExecute 指向 Explorer.exe 来启动目标程序,或者利用 CreateProcessWithTokenW 获取 Explorer 的 Token。

  • 意义: 这样可以避免 Quicker 的高级权限“污染”子进程,解决这类工具软件在 UIAccess 模式下的兼容性顽疾。

 

然后提供一下我的中转程序的c#代码介绍. 可以供老大参考:

 

# UIAccess 剥离/自检逻辑分析(编辑器启动)

## 结论概览(这个项目“怎么做到剥离 UIAccess”)

这个项目的做法并不是“获取 UIAccess”,而是**在启动时检测当前进程是否意外带有 UIAccess**,如果是,则**立刻用 Explorer(桌面壳进程)的普通用户令牌重新启动自身**,从而把进程拉回到“正常桌面用户态”(即无 UIAccess / 普通 token)。

也就是说:

- **检测**:通过 `GetTokenInformation(TokenUIAccess)` 判断当前进程 token 是否包含 UIAccess。
- **剥离**:若检测为 UIAccess,则
  - 找到同 Session 的 `explorer.exe`
  - `OpenProcessToken` 拿到 explorer token
  - `DuplicateTokenEx` 复制成 Primary Token
  - `CreateProcessWithTokenW` 用该 Primary Token 启动本程序
  - 当前实例直接 `Shutdown/Exit`

该方案的关键点:

- UIAccess 属性属于 token 侧能力,**不能在同一进程内“关掉 UIAccess”**;因此只能通过**换 token 重启进程**来实现“剥离”。
- 选择 Explorer token 的原因:Explorer 代表当前交互桌面用户(同会话),属于“正常、非 UIAccess”的典型用户 token 来源。

---

## 入口位置(权威逻辑在哪里)

- 文件:`App.xaml.cs`
- 方法:`Application_Startup(...)`
- 关键调用:

```csharp
// 启动自检:若当前进程被以 UIAccess 启动(例如第三方工具导致),则用 Explorer 用户令牌重启自身,恢复到正常桌面用户态。
if (SelfRelaunchAsExplorerUser.RelaunchIfUiAccess(e?.Args))
{
    Shutdown();
    return;
}
```

说明:只要 `RelaunchIfUiAccess` 返回 `true`,就表示已完成“用 Explorer 用户态拉起新实例”的动作,当前实例应立即退出,避免双实例并行。

---

## 1) UIAccess 检测:IsCurrentProcessUiAccess

位置:`App.xaml.cs` -> `SelfRelaunchAsExplorerUser.IsCurrentProcessUiAccess()`

```csharp
private static bool IsCurrentProcessUiAccess()
{
    IntPtr hToken = IntPtr.Zero;
    try
    {
        if (!OpenProcessToken(Process.GetCurrentProcess().Handle, TOKEN_QUERY, out hToken))
            return false;

        int uiAccess = 0;
        int retLen = 0;
        bool ok = GetTokenInformation(
            hToken,
            TOKEN_INFORMATION_CLASS.TokenUIAccess,
            out uiAccess,
            sizeof(int),
            out retLen);

        if (!ok) return false;
        return uiAccess != 0;
    }
    finally
    {
        if (hToken != IntPtr.Zero) CloseHandle(hToken);
    }
}
```

关键点:

- `TOKEN_INFORMATION_CLASS.TokenUIAccess = 26`
- `GetTokenInformation(..., TokenUIAccess, ...)` 返回的 `uiAccess != 0` 即视为当前进程为 UIAccess token。

---

## 2) 防无限重启:--relaunch-clean

位置:`App.xaml.cs` -> `SelfRelaunchAsExplorerUser.RelaunchIfUiAccess(...)`

```csharp
private const string RelaunchFlag = "--relaunch-clean";

if (args.Any(a => string.Equals(a, RelaunchFlag, StringComparison.OrdinalIgnoreCase)))
    return false;
```

含义:

- 新进程启动时会在原参数基础上追加 `--relaunch-clean`
- 如果检测到该标记,则不再触发重启(避免异常情况下进入循环重启)

---

## 3) 用 Explorer 用户 token 重启:LaunchAsExplorerUser

### 3.1 获取 Explorer token:GetExplorerUserToken

```csharp
private static IntPtr GetExplorerUserToken()
{
    int mySession = Process.GetCurrentProcess().SessionId;

    Process explorer = Process.GetProcessesByName("explorer")
        .Where(p =>
        {
            try { return p.SessionId == mySession; }
            catch { return false; }
        })
        .OrderBy(p => p.Id)
        .FirstOrDefault();

    if (explorer == null)
        explorer = Process.GetProcessesByName("explorer").OrderBy(p => p.Id).FirstOrDefault();

    if (explorer == null)
        throw new Exception("explorer.exe not found.");

    IntPtr hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, explorer.Id);
    if (hProcess == IntPtr.Zero)
        throw new Win32Exception(Marshal.GetLastWin32Error(), "OpenProcess(explorer) failed.");

    try
    {
        IntPtr hToken;
        if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE | TOKEN_QUERY, out hToken))
            throw new Win32Exception(Marshal.GetLastWin32Error(), "OpenProcessToken(explorer) failed.");
        return hToken;
    }
    finally
    {
        CloseHandle(hProcess);
    }
}
```

要点:

- 优先选择**同 Session** 的 Explorer(更贴近当前交互桌面)
- 拿到的是 Explorer 的 **Process Token**(后续还要 Duplicate 成 Primary Token)

### 3.2 Duplicate 为 Primary Token + CreateProcessWithTokenW

```csharp
private static void LaunchAsExplorerUser(string cmdLine, string workDir)
{
    IntPtr hUserToken = IntPtr.Zero;
    IntPtr hPrimaryToken = IntPtr.Zero;
    IntPtr pEnv = IntPtr.Zero;

    try
    {
        hUserToken = GetExplorerUserToken();
        if (hUserToken == IntPtr.Zero)
            throw new Win32Exception(Marshal.GetLastWin32Error(), "GetExplorerUserToken failed.");

        if (!DuplicateTokenEx(
                hUserToken,
                TOKEN_ALL_ACCESS,
                IntPtr.Zero,
                SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
                TOKEN_TYPE.TokenPrimary,
                out hPrimaryToken))
        {
            throw new Win32Exception(Marshal.GetLastWin32Error(), "DuplicateTokenEx failed.");
        }

        if (!CreateEnvironmentBlock(out pEnv, hPrimaryToken, false))
            pEnv = IntPtr.Zero;

        STARTUPINFO si = new STARTUPINFO();
        si.cb = Marshal.SizeOf(typeof(STARTUPINFO));
        si.lpDesktop = @"winsta0\default";

        PROCESS_INFORMATION pi;

        uint flags = CREATE_UNICODE_ENVIRONMENT;

        bool ok = CreateProcessWithTokenW(
            hPrimaryToken,
            LogonFlags.WithProfile,
            null,
            cmdLine,
            flags,
            pEnv == IntPtr.Zero ? IntPtr.Zero : pEnv,
            workDir,
            ref si,
            out pi);

        if (!ok)
            throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateProcessWithTokenW failed.");

        CloseHandle(pi.hThread);
        CloseHandle(pi.hProcess);
    }
    finally
    {
        if (pEnv != IntPtr.Zero) DestroyEnvironmentBlock(pEnv);
        if (hPrimaryToken != IntPtr.Zero) CloseHandle(hPrimaryToken);
        if (hUserToken != IntPtr.Zero) CloseHandle(hUserToken);
    }
}
```

关键点:

- `DuplicateTokenEx(..., TokenPrimary, out hPrimaryToken)`:`CreateProcessWithTokenW` 需要 Primary Token。
- `CreateEnvironmentBlock/DestroyEnvironmentBlock`:为新进程准备用户环境变量(失败则退化为 `null`)。
- `si.lpDesktop = "winsta0\default"`:确保进程在交互桌面启动。

---

## 4) 组装命令行(保留原参数 + 防环标记)

```csharp
var cmdLine = BuildCommandLine(new[] { exePath }.Concat(args).Concat(new[] { RelaunchFlag }).ToArray());
```

其中 `BuildCommandLine/Quote` 负责对参数做引号转义,避免路径空格导致启动失败。

---

## 相关 Win32 / PInvoke 常量与枚举(参数参考)

```csharp
private const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;

private const uint TOKEN_QUERY = 0x0008;
private const uint TOKEN_DUPLICATE = 0x0002;
private const uint TOKEN_ALL_ACCESS = 0xF01FF;

private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;

private enum TOKEN_INFORMATION_CLASS
{
    TokenUIAccess = 26,
}
```

以及关键 API:

- `OpenProcess`
- `OpenProcessToken`
- `GetTokenInformation`
- `DuplicateTokenEx`
- `CreateProcessWithTokenW`
- `CreateEnvironmentBlock` / `DestroyEnvironmentBlock`
- `CloseHandle`

---

## 与 UI(MainWindow)关系

`MainWindow.xaml / MainWindow.xaml.cs` 主要是启动器 UI 与交互逻辑(NoActivate、Topmost、全局热键、鼠标/键盘 Hook 等),**并不负责 UIAccess 剥离**。

UIAccess 相关逻辑全部集中在 `App.xaml.cs` 的 `SelfRelaunchAsExplorerUser`。

---

## 风险点/注意事项(用作参数时建议写明)

  • **该实现依赖当前系统存在 `explorer.exe` 且可访问其 token**。
    - 在某些受限环境(例如服务会话、无 Explorer、权限策略限制)下,`OpenProcessToken(explorer)` 或 `CreateProcessWithTokenW` 可能失败;代码当前策略是失败则放行(不拦截启动)。
    - 这是“从 UIAccess 回退到普通用户态”的策略;如果目标是“获得 UIAccess”,需要完全不同的签名/安装目录/manifest 策略(该项目未实现)。

 


回复内容
CL 4小时45分钟前
#1

感谢反馈,我研究下,之前确实有一些情况qk启动的程序工作不太正常。

暖暖~ 24 分钟前
#2

崔大, 我刚才根据这个UIAccess的原理. 设计写了个winform程序来复现这个窗口错乱的情况. 里面包含源码和生成的exe程序

https://wwaml.lanzoul.com/iGe533idwuli

这个测试程序就是如果正常双击exe打开. 三个窗口层级关系是正常的. 主窗口在下面. 两个小窗口在上方. 主窗口点击按钮会弹出两个窗口. 可以依次关闭


如果用Quicker打开, 主窗口的层级会不对. 因为主窗口设置了一个循环置顶. 所以会盖住原来全部的子窗口, 然后点击主窗口按钮后. 因为会重复盖住后面出现的模态窗口. 会导致全部窗口无法移动



另外祝崔大新年快乐!  过年吃好喝好! 祝Quicker新的一年越来越好!

回复主贴