зеркало из https://github.com/mozilla/gecko-dev.git
277 строки
9.2 KiB
C++
277 строки
9.2 KiB
C++
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=2 et sw=2 tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "NotificationCallback.h"
|
|
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "mozilla/CmdLineAndEnvUtils.h"
|
|
#include "mozilla/ToastNotificationHeaderOnlyUtils.h"
|
|
|
|
using namespace mozilla::widget::toastnotification;
|
|
|
|
HRESULT STDMETHODCALLTYPE
|
|
NotificationCallback::QueryInterface(REFIID riid, void** ppvObject) {
|
|
if (!ppvObject) {
|
|
return E_POINTER;
|
|
}
|
|
|
|
*ppvObject = nullptr;
|
|
|
|
if (!(riid == guid || riid == __uuidof(INotificationActivationCallback) ||
|
|
riid == __uuidof(IUnknown))) {
|
|
return E_NOINTERFACE;
|
|
}
|
|
|
|
AddRef();
|
|
*ppvObject = reinterpret_cast<void*>(this);
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT STDMETHODCALLTYPE NotificationCallback::Activate(
|
|
LPCWSTR appUserModelId, LPCWSTR invokedArgs,
|
|
const NOTIFICATION_USER_INPUT_DATA* data, ULONG dataCount) {
|
|
HandleActivation(invokedArgs);
|
|
|
|
// Windows 8 style callbacks are not called and notifications are not removed
|
|
// from the Action Center unless we return `S_OK`, so always do so even if
|
|
// we're unable to handle the notification properly.
|
|
return S_OK;
|
|
}
|
|
|
|
void NotificationCallback::HandleActivation(LPCWSTR invokedArgs) {
|
|
auto maybeArgs = ParseToastArguments(invokedArgs);
|
|
if (maybeArgs) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Info,
|
|
(L"Invoked with arguments: '%s'", invokedArgs));
|
|
} else {
|
|
NOTIFY_LOG(mozilla::LogLevel::Info, (L"COM server disabled for toast"));
|
|
return;
|
|
}
|
|
const auto& args = maybeArgs.value();
|
|
auto [programPath, cmdLine] = BuildRunCommand(args);
|
|
|
|
// This pipe object will let Firefox notify us when it has handled the
|
|
// notification. Create this before interacting with the application so the
|
|
// application can rely on it existing.
|
|
auto maybePipe = CreatePipe(args.windowsTag);
|
|
|
|
// Run the application.
|
|
|
|
STARTUPINFOW si = {};
|
|
si.cb = sizeof(STARTUPINFOW);
|
|
PROCESS_INFORMATION pi = {};
|
|
|
|
// Runs `{program path} [--profile {profile path}] [--notification-windowsTag
|
|
// {tag}]`.
|
|
CreateProcessW(programPath.c_str(), cmdLine.get(), nullptr, nullptr, false,
|
|
DETACHED_PROCESS | NORMAL_PRIORITY_CLASS, nullptr, nullptr,
|
|
&si, &pi);
|
|
|
|
NOTIFY_LOG(mozilla::LogLevel::Info, (L"Invoked %s", cmdLine.get()));
|
|
|
|
// Transfer `SetForegroundWindow` permission to the launched application.
|
|
|
|
maybePipe.apply([](const auto& pipe) {
|
|
if (ConnectPipeWithTimeout(pipe)) {
|
|
HandlePipeMessages(pipe);
|
|
}
|
|
});
|
|
}
|
|
|
|
mozilla::Maybe<ToastArgs> NotificationCallback::ParseToastArguments(
|
|
LPCWSTR invokedArgs) {
|
|
ToastArgs parsedArgs;
|
|
std::wistringstream args(invokedArgs);
|
|
bool serverDisabled = true;
|
|
|
|
for (std::wstring key, value;
|
|
std::getline(args, key) && std::getline(args, value);) {
|
|
if (key == kLaunchArgProgram) {
|
|
serverDisabled = false;
|
|
} else if (key == kLaunchArgProfile) {
|
|
parsedArgs.profile = value;
|
|
} else if (key == kLaunchArgTag) {
|
|
parsedArgs.windowsTag = value;
|
|
} else if (key == kLaunchArgLogging) {
|
|
gVerbose = value == L"verbose";
|
|
} else if (key == kLaunchArgAction) {
|
|
parsedArgs.action = value;
|
|
}
|
|
}
|
|
|
|
if (serverDisabled) {
|
|
return mozilla::Nothing();
|
|
}
|
|
|
|
return mozilla::Some(parsedArgs);
|
|
}
|
|
|
|
std::tuple<path, mozilla::UniquePtr<wchar_t[]>>
|
|
NotificationCallback::BuildRunCommand(const ToastArgs& args) {
|
|
path programPath = installDir / L"" MOZ_APP_NAME;
|
|
programPath += L".exe";
|
|
|
|
std::vector<const wchar_t*> childArgv;
|
|
childArgv.push_back(programPath.c_str());
|
|
|
|
if (!args.profile.empty()) {
|
|
childArgv.push_back(L"--profile");
|
|
childArgv.push_back(args.profile.c_str());
|
|
} else {
|
|
NOTIFY_LOG(mozilla::LogLevel::Warning,
|
|
(L"No profile; invocation will choose default profile"));
|
|
}
|
|
|
|
if (!args.windowsTag.empty()) {
|
|
childArgv.push_back(L"--notification-windowsTag");
|
|
childArgv.push_back(args.windowsTag.c_str());
|
|
} else {
|
|
NOTIFY_LOG(mozilla::LogLevel::Warning, (L"No windowsTag; invoking anyway"));
|
|
}
|
|
|
|
if (!args.action.empty()) {
|
|
childArgv.push_back(L"--notification-windowsAction");
|
|
childArgv.push_back(args.action.c_str());
|
|
} else {
|
|
NOTIFY_LOG(mozilla::LogLevel::Warning, (L"No action; invoking anyway"));
|
|
}
|
|
|
|
return {programPath,
|
|
mozilla::MakeCommandLine(childArgv.size(), childArgv.data())};
|
|
}
|
|
|
|
mozilla::Maybe<nsAutoHandle> NotificationCallback::CreatePipe(
|
|
const std::wstring& tag) {
|
|
if (tag.empty()) {
|
|
return mozilla::Nothing();
|
|
}
|
|
|
|
// Prefix required by pipe API.
|
|
std::wstring pipeName = GetNotificationPipeName(tag.c_str());
|
|
|
|
nsAutoHandle pipe(CreateNamedPipeW(
|
|
pipeName.c_str(), PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
|
|
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT |
|
|
PIPE_REJECT_REMOTE_CLIENTS,
|
|
1, sizeof(ToastNotificationPermissionMessage),
|
|
sizeof(ToastNotificationPidMessage), 0, nullptr));
|
|
if (pipe.get() == INVALID_HANDLE_VALUE) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Error, (L"Error creating pipe %s, error %lu",
|
|
pipeName.c_str(), GetLastError()));
|
|
return mozilla::Nothing();
|
|
}
|
|
|
|
return mozilla::Some(pipe.out());
|
|
}
|
|
|
|
bool NotificationCallback::ConnectPipeWithTimeout(const nsAutoHandle& pipe) {
|
|
nsAutoHandle overlappedEvent(CreateEventW(nullptr, TRUE, FALSE, nullptr));
|
|
if (!overlappedEvent) {
|
|
NOTIFY_LOG(
|
|
mozilla::LogLevel::Error,
|
|
(L"Error creating pipe connect event, error %lu", GetLastError()));
|
|
return false;
|
|
}
|
|
|
|
OVERLAPPED overlappedConnect{};
|
|
overlappedConnect.hEvent = overlappedEvent.get();
|
|
|
|
BOOL result = ConnectNamedPipe(pipe.get(), &overlappedConnect);
|
|
DWORD lastError = GetLastError();
|
|
if (lastError == ERROR_IO_PENDING) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Info, (L"Waiting on pipe connection"));
|
|
|
|
if (!WaitEventWithTimeout(overlappedEvent)) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Warning,
|
|
(L"Pipe connect wait failed, cancelling (connection may still "
|
|
L"succeed)"));
|
|
|
|
CancelIo(pipe.get());
|
|
DWORD undefined;
|
|
BOOL overlappedResult =
|
|
GetOverlappedResult(pipe.get(), &overlappedConnect, &undefined, TRUE);
|
|
if (!overlappedResult || GetLastError() != ERROR_PIPE_CONNECTED) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Error,
|
|
(L"Pipe connect failed, error %lu", GetLastError()));
|
|
return false;
|
|
}
|
|
|
|
// Pipe connected before cancellation, fall through.
|
|
}
|
|
} else if (result) {
|
|
// Overlapped `ConnectNamedPipe` should return 0.
|
|
NOTIFY_LOG(mozilla::LogLevel::Error,
|
|
(L"Error connecting pipe, error %lu", lastError));
|
|
return false;
|
|
} else if (lastError != ERROR_PIPE_CONNECTED) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Error,
|
|
(L"Error connecting pipe, error %lu", lastError));
|
|
return false;
|
|
}
|
|
|
|
NOTIFY_LOG(mozilla::LogLevel::Info, (L"Pipe connected!"));
|
|
return true;
|
|
}
|
|
|
|
void NotificationCallback::HandlePipeMessages(const nsAutoHandle& pipe) {
|
|
ToastNotificationPidMessage in{};
|
|
auto read = [&](OVERLAPPED& overlapped) {
|
|
return ReadFile(pipe.get(), &in, sizeof(in), nullptr, &overlapped);
|
|
};
|
|
if (!SyncDoOverlappedIOWithTimeout(pipe, sizeof(in), read)) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Error, (L"Pipe read failed"));
|
|
return;
|
|
}
|
|
|
|
ToastNotificationPermissionMessage out{};
|
|
out.setForegroundPermissionGranted = TransferForegroundPermission(in.pid);
|
|
auto write = [&](OVERLAPPED& overlapped) {
|
|
return WriteFile(pipe.get(), &out, sizeof(out), nullptr, &overlapped);
|
|
};
|
|
if (!SyncDoOverlappedIOWithTimeout(pipe, sizeof(out), write)) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Error, (L"Pipe write failed"));
|
|
return;
|
|
}
|
|
|
|
NOTIFY_LOG(mozilla::LogLevel::Info, (L"Pipe write succeeded!"));
|
|
}
|
|
|
|
DWORD NotificationCallback::TransferForegroundPermission(DWORD pid) {
|
|
// When the instance of Firefox is still running we need to grant it
|
|
// foreground permission to bring itself to the foreground. We're able to do
|
|
// this even though the COM server is not the foreground process likely due to
|
|
// Windows granting permission to the COM object via
|
|
// `CoAllowSetForegroundWindow`.
|
|
//
|
|
// Note that issues surrounding `SetForegroundWindow` permissions are obscured
|
|
// when builds are run with a debugger, whereupon Windows grants
|
|
// `SetForegroundWindow` permission in all instances.
|
|
//
|
|
// We can not rely on granting this permission to the process created above
|
|
// because remote server clients do not meet the criteria to receive
|
|
// `SetForegroundWindow` permissions without unsupported hacks.
|
|
if (!pid) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Warning,
|
|
(L"`pid` received from pipe was 0, no process to grant "
|
|
L"`SetForegroundWindow` permission to"));
|
|
return FALSE;
|
|
}
|
|
// When this call succeeds, the COM process loses the `SetForegroundWindow`
|
|
// permission.
|
|
if (!AllowSetForegroundWindow(pid)) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Error,
|
|
(L"Failed to grant `SetForegroundWindow` permission, error %lu",
|
|
GetLastError()));
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|