诊断和解决 ToastNotification 异常

Diagnosing and Resolving a ToastNotification Exception

问题:
我的简单 Windows 通知应用程序抛出错误消息“访问被拒绝”的异常。我该如何解决这个问题?

前言:
使用的结构由当前应用程序的实现和限制规定。虽然可能存在比这更好的结构,但目前无法更改结构。

相关结构信息:
我们的服务总是 运行s。该服务将为机器上的每个活动会话 ID 生成一个进程——让我们称之为 data.exe——。例如,如果服务器上当前有四个会话 ID 为 1-4 的活动会话,则所有 4 个会话的 data.exe 将为 运行ning。值得注意的是,尽管 运行 在非零会话 ID 中,每个进程在任务管理器中的用户名都是 SYSTEM。

信息:
我的应用程序——toaster.exe——应该通过 CreateProcessW(). The full launch function is adapted from a Raymond Chen blog post found here 从不同的进程启动。作为此函数功能的简要说明,它启动了一个程序,其中 shell 作为其父进程。为清楚起见,我将此功能实现到 data.exe 中以启动 toaster.exe,我所做的调整只是为了处理错误,显然,是为了启动我想要的特定程序,而不是 cmd.exe .陈先生的功能是我发现的唯一可以成功可靠地启动我的应用程序并使其按预期运行的方法。

目前,如果我通过单击启动 toaster.exe,它可以完美地执行其功能。我还可以确认我能够从另一个进程启动 toaster.exe——我设置了一个测试应用程序,它可以以不抛出异常的方式成功启动 toaster.exe。为了阐明我所说的“功能完美”的意思,toaster.exe 成功地向我发送了一条简单的单行通知,其中包含一条我为测试目的而硬编码的消息。

但是,如果我从我们的服务启动的程序启动 toaster.exe,它会在尝试创建 winrt::Windows::UI::Notifications::ToastNotification 对象时遇到异常。不幸的是,最后一个是我唯一关心的启动方法——这个程序 需要 兼容以这种方式启动,因为它总是由 data.exe 启动,如前言所述,由我们的服务启动。

这里是toaster.exe中发生异常的函数:

using namespace winrt;
using namespace Windows::Data::Xml::Dom;
using namespace Windows::UI::Notifications;

void SendBasicToast(const std::wstring &strMessage)
{
    // Construct the toast template
    XmlDocument doc;
    doc.LoadXml(L"<toast>\
    <visual>\
        <binding template=\"ToastGeneric\">\
            <text></text>\
        </binding>\
    </visual>\
</toast>");

    // Populate with text and values
    doc.SelectSingleNode(L"//text[1]").InnerText(strMessage);

    // Construct the notification [THIS LINE CAUSES AN EXCEPTION]
    ToastNotification notif{ doc };

    // And send it!
    DesktopNotificationManagerCompat::CreateToastNotifier().Show(notif);
}

DesktopNotificationManagerCompat.h 来自 this Microsoft repository, which is supplemental material to their local toast notification tutorial。为了信息的完整性,我会注意到 DesktopNotificationManagerCompat::CreateToastNotifier()ToastNotificationManager::CreateToastNotifier() 的简单包装器,定义如下:

ToastNotifier DesktopNotificationManagerCompat::CreateToastNotifier()
if (HasIdentity())
    {
        return ToastNotificationManager::CreateToastNotifier();
    }
    else
    {
        return ToastNotificationManager::CreateToastNotifier(_win32Aumid);
    }
}

我已经尝试删除这个包装函数并直接调用 CreateToastNotifier() 并将我的 AUMID 作为参数,但这也导致抛出异常。然而,这并不是严格相关的,因为异常是在上一行抛出的。

我调用toast函数的方式:

try
{
    std::wstring strMessage{ L"<(^.^<) || Default Toast || (>^.^)>" };
    SendBasicToast(strMessage);
}
catch(winrt::hresult_error &e)
{
    //Log the exception message
}

方式 toaster.exe 将通过 data.exe 启动,如上段所述(如果用户在通知托盘中单击它,但这超出了这个问题的范围)。但是,正如这个问题所证明的那样,在我需要它工作的特定情况下,它并没有按预期工作。我已经将问题诊断为单行(请记住我实际上是 using namespace winrt::Windows::UI::Notifications):

ToastNotification notif{ doc };

编辑:
感谢 @IInspectable 暗示异常可能具有类型 winrt::hresult_error,我能够辨别出我从 ToastNotification 构造函数收到的错误消息是“访问被拒绝”。通常,我希望这需要提升权限才能解决,但是当我自己 运行 程序时,它 运行 具有标准权限并且没有问题。哪些访问被拒绝,我该如何获得?

Microsoft Q&A Page for reference

预编辑异常故障排除:
[此部分不再严格相关]
我在 WinRT(仅 C#)中找不到关于此 class 的任何 MSDN 文档,并查看 windows.ui.notifications.2.h--当我在 ToastNotification 上单击“转到定义”时 Visual Studio 打开的文件- -我无法辨别此构造函数可能抛出的异常类型。因此,我知道抛出异常的行,但我不知道异常是什么类型,因此我不知道它为什么抛出这个异常,也不知道如何修复它。我不记得我在 catch 之后放在括号中的所有内容,但我知道我曾尝试假设它是 std::exceptionconst char*const wchar_t*。尝试将 catch 块与所有这些一起使用导致崩溃。

我不知道如何诊断这个问题,更不用说修复它了。我掌握的最好的信息是只有当 toaster.exe 从 data.exe 启动时才会发生此异常,但我不知道如何处理该信息。

编辑 2022/2/28:
由于 this SO answer.

,我找到了一个更优雅的解决方案来解决这个涉及 WTSQueryToken()GetTokenInformation() 的问题

WTSQueryToken() returns 过滤后的用户令牌,无需任何额外步骤,可以将此令牌放入 CreateProcessAsUserW() 调用以生成我的通知应用程序。

我选择调用 GetTokenInformation() 来为用户获取未过滤的(读取:提升的)令牌,这使我可以将所有应用程序的日志集中在 Program Files 目录中。我不清楚使用没有管理员权限的用户的未过滤令牌启动的进程是否仍然能够登录到 Program Files 目录,但这是 non-essential 进程能够 运行.

解决方案:
为抛出的异常给出的错误消息是 Access is denied。这可能是也可能不是与 PROC_THREAD_ATTRIBUTE_PARENT_PROCESS 标志相关的错误的结果。解决方法是简单地从子进程再次生成进程,使用完全相同的函数调用减去有问题的标志。

在我的例子中,我传递了一个参数作为标志,告诉子进程它需要重新启动自己。

解释:
感谢 @IInspectable 的指导,我得以解决我的问题。

首先,IInspectable 告诉我异常的数据类型可能是 winrt::hresult_error,结果是正确的。由此,我能够看到异常消息是 Access is denied.

更新我的问题以反映这一点后,IInspectable 返回指向我在我的问题中链接的 Raymond Chen 博客 post 的 the bottom of the comments

简而言之,该评论表明 API 中存在一个潜在错误,导致以 Chen 演示的方式生成的进程加载不正确的参数,并通过使用 Chen 的函数提供了一个简单的解决方法在子进程中启动另一个子进程。该评论还建议从第二个函数调用中删除 PROC_THREAD_ATTRIBUTE_PARENT_PROCESS 标志是必要的,但这不是我为使变通方法成功而需要做的事情。

为了posterity,来自博客post的评论如下:

There are issues using the PROC_THREAD_ATTRIBUTE_PARENT_PROCESS flag… Environment variables for the new process are copied from the High-IL process not the Medium-IL process which cause issues with shell functions and some directory paths (%temp%, %userprofile% etc…) The token security descriptor for the process is also created without an ACE for the current user blocking access to most resources and the token security descriptor will also have a High-IL even though the process itself has a Medium-IL… This blocks the new process from accessing any system objects (events/pipes/sections/IPC etc…) while also blocking the process from opening its own process token with TOKEN_QUERY access. This seems to be a severe bug with the API but not sure if it’ll be fixed. As a workaround you can call CreateProcess a second time but from the new child process (without the PARENT_PROCESS flag) and it’ll be created with the correct token DAC security and environment variables (this is also why the above above sample doesn’t have issues with cmd.exe since it launches child processes).