summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/defaultagent/main.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/defaultagent/main.cpp')
-rw-r--r--toolkit/mozapps/defaultagent/main.cpp428
1 files changed, 428 insertions, 0 deletions
diff --git a/toolkit/mozapps/defaultagent/main.cpp b/toolkit/mozapps/defaultagent/main.cpp
new file mode 100644
index 0000000000..42eabb8d77
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/main.cpp
@@ -0,0 +1,428 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 <windows.h>
+#include <shlwapi.h>
+#include <objbase.h>
+#include <string.h>
+#include <iostream>
+
+#include "nsAutoRef.h"
+#include "nsWindowsHelpers.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+
+#include "common.h"
+#include "Policy.h"
+#include "DefaultBrowser.h"
+#include "DefaultPDF.h"
+#include "EventLog.h"
+#include "Notification.h"
+#include "Registry.h"
+#include "RemoteSettings.h"
+#include "ScheduledTask.h"
+#include "SetDefaultBrowser.h"
+#include "Telemetry.h"
+
+// The AGENT_REGKEY_NAME is dependent on MOZ_APP_VENDOR and MOZ_APP_BASENAME,
+// so using those values in the mutex name prevents waiting on processes that
+// are using completely different data.
+#define REGISTRY_MUTEX_NAME \
+ L"" MOZ_APP_VENDOR MOZ_APP_BASENAME L"DefaultBrowserAgentRegistryMutex"
+// How long to wait on the registry mutex before giving up on it. This should
+// be short. Although the WDBA runs in the background, uninstallation happens
+// synchronously in the foreground.
+#define REGISTRY_MUTEX_TIMEOUT_MS (3 * 1000)
+
+// Returns true if the registry value name given is one of the
+// install-directory-prefixed values used by the Windows Default Browser Agent.
+// ex: "C:\Program Files\Mozilla Firefox|PreviousDefault"
+// Returns true
+// ex: "InitialNotificationShown"
+// Returns false
+static bool IsPrefixedValueName(const wchar_t* valueName) {
+ // Prefixed value names use '|' as a delimiter. None of the
+ // non-install-directory-prefixed value names contain one.
+ return wcschr(valueName, L'|') != nullptr;
+}
+
+static void RemoveAllRegistryEntries() {
+ mozilla::UniquePtr<wchar_t[]> installPath = mozilla::GetFullBinaryPath();
+ if (!PathRemoveFileSpecW(installPath.get())) {
+ return;
+ }
+
+ HKEY rawRegKey = nullptr;
+ if (ERROR_SUCCESS !=
+ RegOpenKeyExW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME, 0,
+ KEY_WRITE | KEY_QUERY_VALUE | KEY_WOW64_64KEY,
+ &rawRegKey)) {
+ return;
+ }
+ nsAutoRegKey regKey(rawRegKey);
+
+ DWORD maxValueNameLen = 0;
+ if (ERROR_SUCCESS != RegQueryInfoKeyW(regKey.get(), nullptr, nullptr, nullptr,
+ nullptr, nullptr, nullptr, nullptr,
+ &maxValueNameLen, nullptr, nullptr,
+ nullptr)) {
+ return;
+ }
+ // The length that RegQueryInfoKeyW returns is without a terminator.
+ maxValueNameLen += 1;
+
+ mozilla::UniquePtr<wchar_t[]> valueName =
+ mozilla::MakeUnique<wchar_t[]>(maxValueNameLen);
+
+ DWORD valueIndex = 0;
+ // Set this to true if we encounter values in this key that are prefixed with
+ // different install directories, indicating that this key is still in use
+ // by other installs.
+ bool keyStillInUse = false;
+
+ while (true) {
+ DWORD valueNameLen = maxValueNameLen;
+ LSTATUS ls =
+ RegEnumValueW(regKey.get(), valueIndex, valueName.get(), &valueNameLen,
+ nullptr, nullptr, nullptr, nullptr);
+ if (ls != ERROR_SUCCESS) {
+ break;
+ }
+
+ if (!wcsnicmp(valueName.get(), installPath.get(),
+ wcslen(installPath.get()))) {
+ RegDeleteValue(regKey.get(), valueName.get());
+ // Only increment the index if we did not delete this value, because if
+ // we did then the indexes of all the values after that one just got
+ // decremented, meaning the index we already have now refers to a value
+ // that we haven't looked at yet.
+ } else {
+ valueIndex++;
+ if (IsPrefixedValueName(valueName.get())) {
+ // If this is not one of the unprefixed value names, it must be one of
+ // the install-directory prefixed values.
+ keyStillInUse = true;
+ }
+ }
+ }
+
+ regKey.reset();
+
+ // If no other installs are using this key, remove it now.
+ if (!keyStillInUse) {
+ // Use RegDeleteTreeW to remove the cache as well, which is in subkey.
+ RegDeleteTreeW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME);
+ }
+}
+
+// This function adds a registry value with this format:
+// <install-dir>|Installed=1
+// RemoveAllRegistryEntries() determines whether the registry key is in use
+// by other installations by checking for install-directory-prefixed value
+// names. Although Firefox mirrors some preferences into install-directory-
+// prefixed values, the WDBA no longer uses any prefixed values. Adding this one
+// makes uninstallation work as expected slightly more reliably.
+static void WriteInstallationRegistryEntry() {
+ mozilla::WindowsErrorResult<mozilla::Ok> result =
+ RegistrySetValueBool(IsPrefixed::Prefixed, L"Installed", true);
+ if (result.isErr()) {
+ LOG_ERROR_MESSAGE(L"Failed to write installation registry entry: %#X",
+ result.unwrapErr().AsHResult());
+ }
+}
+
+// This class is designed to prevent concurrency problems when accessing the
+// registry. It should be acquired before any usage of unprefixed registry
+// entries.
+class RegistryMutex {
+ private:
+ nsAutoHandle mMutex;
+ bool mLocked;
+
+ public:
+ RegistryMutex() : mMutex(nullptr), mLocked(false) {}
+ ~RegistryMutex() {
+ Release();
+ // nsAutoHandle will take care of closing the mutex's handle.
+ }
+
+ // Returns true on success, false on failure.
+ bool Acquire() {
+ if (mLocked) {
+ return true;
+ }
+
+ if (mMutex.get() == nullptr) {
+ // It seems like we would want to set the second parameter (bInitialOwner)
+ // to TRUE, but the documentation for CreateMutexW suggests that, because
+ // we aren't sure that the mutex doesn't already exist, we can't be sure
+ // whether we got ownership via this mechanism.
+ mMutex.own(CreateMutexW(nullptr, FALSE, REGISTRY_MUTEX_NAME));
+ if (mMutex.get() == nullptr) {
+ LOG_ERROR_MESSAGE(L"Couldn't open registry mutex: %#X", GetLastError());
+ return false;
+ }
+ }
+
+ DWORD mutexStatus =
+ WaitForSingleObject(mMutex.get(), REGISTRY_MUTEX_TIMEOUT_MS);
+ if (mutexStatus == WAIT_OBJECT_0) {
+ mLocked = true;
+ } else if (mutexStatus == WAIT_TIMEOUT) {
+ LOG_ERROR_MESSAGE(L"Timed out waiting for registry mutex");
+ } else if (mutexStatus == WAIT_ABANDONED) {
+ // This isn't really an error for us. No one else is using the registry.
+ // This status code means that we are supposed to check our data for
+ // consistency, but there isn't really anything we can fix here.
+ // This is an indication that an agent crashed though, which is clearly an
+ // error, so log an error message.
+ LOG_ERROR_MESSAGE(L"Found abandoned registry mutex. Continuing...");
+ mLocked = true;
+ } else {
+ // The only other documented status code is WAIT_FAILED. In the case that
+ // we somehow get some other code, that is also an error.
+ LOG_ERROR_MESSAGE(L"Failed to wait on registry mutex: %#X",
+ GetLastError());
+ }
+ return mLocked;
+ }
+
+ bool IsLocked() { return mLocked; }
+
+ void Release() {
+ if (mLocked) {
+ if (mMutex.get() == nullptr) {
+ LOG_ERROR_MESSAGE(L"Unexpectedly missing registry mutex");
+ return;
+ }
+ BOOL success = ReleaseMutex(mMutex.get());
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"Failed to release registry mutex");
+ }
+ mLocked = false;
+ }
+ }
+};
+
+// Returns false (without setting aResult) if reading last run time failed.
+static bool CheckIfAppRanRecently(bool* aResult) {
+ const ULONGLONG kTaskExpirationDays = 90;
+ const ULONGLONG kTaskExpirationSeconds = kTaskExpirationDays * 24 * 60 * 60;
+
+ MaybeQwordResult lastRunTimeResult =
+ RegistryGetValueQword(IsPrefixed::Prefixed, L"AppLastRunTime");
+ if (lastRunTimeResult.isErr()) {
+ return false;
+ }
+ mozilla::Maybe<ULONGLONG> lastRunTimeMaybe = lastRunTimeResult.unwrap();
+ if (!lastRunTimeMaybe.isSome()) {
+ return false;
+ }
+
+ ULONGLONG secondsSinceLastRunTime =
+ SecondsPassedSince(lastRunTimeMaybe.value());
+
+ *aResult = secondsSinceLastRunTime < kTaskExpirationSeconds;
+ return true;
+}
+
+// We expect to be given a command string in argv[1], perhaps followed by other
+// arguments depending on the command. The valid commands are:
+// register-task [unique-token]
+// Create a Windows scheduled task that will launch this binary with the
+// do-task command every 24 hours, starting from 24 hours after register-task
+// is run. unique-token is required and should be some string that uniquely
+// identifies this installation of the product; typically this will be the
+// install path hash that's used for the update directory, the AppUserModelID,
+// and other related purposes.
+// update-task [unique-token]
+// Update an existing task registration, without changing its schedule. This
+// should be called during updates of the application, in case this program
+// has been updated and any of the task parameters have changed. The unique
+// token argument is required and should be the same one that was passed in
+// when the task was registered.
+// unregister-task [unique-token]
+// Removes the previously created task. The unique token argument is required
+// and should be the same one that was passed in when the task was registered.
+// uninstall [unique-token]
+// Removes the previously created task, and also removes all registry entries
+// running the task may have created. The unique token argument is required
+// and should be the same one that was passed in when the task was registered.
+// do-task [app-user-model-id]
+// Actually performs the default agent task, which currently means generating
+// and sending our telemetry ping and possibly showing a notification to the
+// user if their browser has switched from Firefox to Edge with Blink.
+// set-default-browser-user-choice [app-user-model-id] [[.file1 ProgIDRoot1]
+// ...]
+// Set the default browser via the UserChoice registry keys. Additional
+// optional file extensions to register can be specified as additional
+// argument pairs: the first element is the file extension, the second element
+// is the root of a ProgID, which will be suffixed with `-$AUMI`.
+int wmain(int argc, wchar_t** argv) {
+ if (argc < 2 || !argv[1]) {
+ return E_INVALIDARG;
+ }
+
+ HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ const struct ComUninitializer {
+ ~ComUninitializer() { CoUninitialize(); }
+ } kCUi;
+
+ RegistryMutex regMutex;
+
+ // The uninstall and unregister commands are allowed even if the policy
+ // disabling the task is set, so that uninstalls and updates always work.
+ // Similarly, debug commands are always allowed.
+ if (!wcscmp(argv[1], L"uninstall")) {
+ if (argc < 3 || !argv[2]) {
+ return E_INVALIDARG;
+ }
+
+ // We aren't actually going to check whether we got the mutex here.
+ // Ideally we would acquire it since we are about to access the registry,
+ // so we would like to block simultaneous users of our registry key.
+ // But there are two reasons that it is preferable to ignore a mutex
+ // wait timeout here:
+ // 1. If we fail to uninstall our prefixed registry entries, the
+ // registry key containing them will never be removed, even when the
+ // last installation is uninstalled.
+ // 2. If we timed out waiting on the mutex, it implies that there are
+ // other installations. If there are other installations, there will
+ // be other prefixed registry entries. If there are other prefixed
+ // registry entries, we won't remove the whole key or touch the
+ // unprefixed entries during uninstallation. Therefore, we should
+ // be able to safely uninstall without stepping on anyone's toes.
+ regMutex.Acquire();
+
+ RemoveAllRegistryEntries();
+ return RemoveTasks(argv[2], WhichTasks::AllTasksForInstallation);
+ } else if (!wcscmp(argv[1], L"unregister-task")) {
+ if (argc < 3 || !argv[2]) {
+ return E_INVALIDARG;
+ }
+
+ return RemoveTasks(argv[2], WhichTasks::WdbaTaskOnly);
+ } else if (!wcscmp(argv[1], L"debug-remote-disabled")) {
+ int disabled = IsAgentRemoteDisabled();
+ std::cerr << "default-browser-agent: IsAgentRemoteDisabled: " << disabled
+ << std::endl;
+ return S_OK;
+ }
+
+ // We check for disablement by policy because that's assumed to be static.
+ // But we don't check for disablement by remote settings so that
+ // `register-task` and `update-task` can proceed as part of the update
+ // cycle, waiting for remote (re-)enablement.
+ if (IsAgentDisabled()) {
+ return HRESULT_FROM_WIN32(ERROR_ACCESS_DISABLED_BY_POLICY);
+ }
+
+ if (!wcscmp(argv[1], L"register-task")) {
+ if (argc < 3 || !argv[2]) {
+ return E_INVALIDARG;
+ }
+ // We aren't actually going to check whether we got the mutex here.
+ // Ideally we would acquire it since registration might migrate registry
+ // entries. But it is preferable to ignore a mutex wait timeout here
+ // because:
+ // 1. Otherwise the task doesn't get registered at all
+ // 2. If another installation's agent is holding the mutex, it either
+ // is far enough out of date that it doesn't yet use the migrated
+ // values, or it already did the migration for us.
+ regMutex.Acquire();
+
+ WriteInstallationRegistryEntry();
+
+ return RegisterTask(argv[2]);
+ } else if (!wcscmp(argv[1], L"update-task")) {
+ if (argc < 3 || !argv[2]) {
+ return E_INVALIDARG;
+ }
+ // Not checking if we got the mutex for the same reason we didn't in
+ // register-task
+ regMutex.Acquire();
+
+ WriteInstallationRegistryEntry();
+
+ return UpdateTask(argv[2]);
+ } else if (!wcscmp(argv[1], L"do-task")) {
+ if (argc < 3 || !argv[2]) {
+ return E_INVALIDARG;
+ }
+
+ bool force = (argc > 3) && ((0 == wcscmp(argv[3], L"--force")) ||
+ (0 == wcscmp(argv[3], L"-force")));
+
+ // Acquire() has a short timeout. Since this runs in the background, we
+ // could use a longer timeout in this situation. However, if another
+ // installation's agent is already running, it will update CurrentDefault,
+ // possibly send a ping, and possibly show a notification.
+ // Once all that has happened, there is no real reason to do it again. We
+ // only send one ping per day, so we aren't going to do that again. And
+ // the only time we ever show a second notification is 7 days after the
+ // first one, so we aren't going to do that again either.
+ // If the other process didn't take those actions, there is no reason that
+ // this process would take them.
+ // If the other process fails, this one will most likely fail for the same
+ // reason.
+ // So we'll just bail if we can't get the mutex quickly.
+ if (!regMutex.Acquire()) {
+ return HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION);
+ }
+
+ // Check that Firefox ran recently, if not then stop here.
+ // Also stop if no timestamp was found, which most likely indicates
+ // that Firefox was not yet run.
+ bool ranRecently = false;
+ if (!force && (!CheckIfAppRanRecently(&ranRecently) || !ranRecently)) {
+ return SCHED_E_TASK_ATTEMPTED;
+ }
+
+ // Check for remote disable and (re-)enable before (potentially)
+ // updating registry entries and showing notifications.
+ if (IsAgentRemoteDisabled()) {
+ return S_OK;
+ }
+
+ DefaultBrowserResult defaultBrowserResult = GetDefaultBrowserInfo();
+ if (defaultBrowserResult.isErr()) {
+ return defaultBrowserResult.unwrapErr().AsHResult();
+ }
+ DefaultBrowserInfo browserInfo = defaultBrowserResult.unwrap();
+
+ DefaultPdfResult defaultPdfResult = GetDefaultPdfInfo();
+ if (defaultPdfResult.isErr()) {
+ return defaultPdfResult.unwrapErr().AsHResult();
+ }
+ DefaultPdfInfo pdfInfo = defaultPdfResult.unwrap();
+
+ NotificationActivities activitiesPerformed =
+ MaybeShowNotification(browserInfo, argv[2], force);
+
+ return SendDefaultBrowserPing(browserInfo, pdfInfo, activitiesPerformed);
+ } else if (!wcscmp(argv[1], L"set-default-browser-user-choice")) {
+ if (argc < 3 || !argv[2]) {
+ return E_INVALIDARG;
+ }
+
+ // `argv` is itself null-terminated, so we can safely pass the tail of the
+ // array here.
+ return SetDefaultBrowserUserChoice(argv[2], &argv[3]);
+ } else if (!wcscmp(argv[1], L"set-default-extension-handlers-user-choice")) {
+ if (argc < 3 || !argv[2]) {
+ return E_INVALIDARG;
+ }
+
+ // `argv` is itself null-terminated, so we can safely pass the tail of the
+ // array here.
+ return SetDefaultExtensionHandlersUserChoice(argv[2], &argv[3]);
+ } else {
+ return E_INVALIDARG;
+ }
+}