Commit 2fb662ca authored by Adam Gashlin's avatar Adam Gashlin
Browse files

Bug 1676296 Part 1: XPCOM component for Windows Task Scheduler. r=mhowell

parent 2bff6d8c
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -110,6 +110,9 @@ DIRS += ["captivedetect"]
if CONFIG["OS_TARGET"] != "Android":
    DIRS += ["terminator"]

if CONFIG["OS_TARGET"] == "WINNT":
    DIRS += ["taskscheduler"]

DIRS += ["build"]

if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
+18 −0
Original line number Diff line number Diff line
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.

Classes = []

if buildconfig.substs['OS_TARGET'] == 'WINNT':
    Classes += [
        {
            'cid': '{e2113dfc-8efe-43a1-8a20-ad720dd771d6}',
            'contract_ids': ['@mozilla.org/win-task-scheduler-service;1'],
            'type': 'nsWinTaskSchedulerService',
            'headers': ['nsWinTaskScheduler.h'],
            'processes': ProcessSelector.MAIN_PROCESS_ONLY,
        },
    ]
+23 −0
Original line number Diff line number Diff line
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.

with Files("**"):
    BUG_COMPONENT = ("Toolkit", "Application Update")

XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"]

XPIDL_MODULE = "taskscheduler"
XPCOM_MANIFESTS += ["components.conf"]

# This whole component is currently Windows-only, but a Mac implementation is planned.
# Only the Windows C++ interface is an XPCOM component.
if CONFIG["OS_TARGET"] == "WINNT":
    XPIDL_SOURCES += ["nsIWinTaskSchedulerService.idl"]
    EXPORTS += ["nsWinTaskScheduler.h"]
    SOURCES += ["nsWinTaskScheduler.cpp"]
    OS_LIBS += ["taskschd"]

FINAL_LIBRARY = "xul"
+104 −0
Original line number Diff line number Diff line
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "nsISupports.idl"

/**
 * An interface for Windows Task Scheduler 2.0.
 * Documentation for the underlying APIs can be found at
 * https://docs.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-start-page
 */
[scriptable, main_process_scriptable_only, uuid(a8d36901-0b6a-46c3-a214-a9e1d5d6047a)]
interface nsIWinTaskSchedulerService : nsISupports
{
  /**
   * Register (create) a task from an XML definition.
   * The task will be created so that it only runs as the current user
   * (TASK_LOGON_INTERACTIVE_TOKEN).
   *
   * @throws NS_ERROR_FILE_NOT_FOUND if the folder does not exist.
   * @throws NS_ERROR_FILE_ALREADY_EXISTS if the task already existed and aUpdateExisting is false.
   *
   * @param aFolderName     Full name of the folder in which to create the task, starting with "\".
   *
   * @param aTaskName       Name of the task.
   *
   * @param aDefinitionXML  XML definition of the task. This is passed directly to Task Scheduler,
   *                        see the schema at
   *                        https://docs.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-schema
   *
   * @param aUpdateExisting Whether to update an existing task with the same name, default false.
   */
  void registerTask(in wstring aFolderName,
                    in wstring aTaskName,
                    in wstring aDefinitionXML,
                    [optional] in boolean aUpdateExisting);

  /**
   * Validate the XML task definition with Task Scheduler without creating a task, for testing.
   * Doesn't throw if only the final ITaskFolder::RegisterTask() fails.
   *
   * @param aDefinitionXML      Definition to validate.
   * @return                    HRESULT from ITaskFolder::RegisterTask()
   *                            Success should be S_OK (0). XML validation failure could be one of
   *                            SCHED_E_UNEXPECTED_NODE, SCHED_E_NAMESPACE, SCHED_E_INVALIDVALUE,
   *                            SCHED_E_MISSINGNODE, SCHED_E_MALFORMEDXML, but there may be others.
   */
  long validateTaskDefinition(in wstring aDefinitionXML);

  /**
   * Get the registration information for a task.
   *
   * @throws NS_ERROR_FILE_NOT_FOUND if the folder or task do not exist.
   *
   * @param aFolderName     Full name of the folder containing the task, starting with "\".
   * @param aTaskName       Name of the task to read.
   * @return                Registration information for the task, as XML text.
   */
  AString getTaskXML(in wstring aFolderName, in wstring aTaskName);

  /**
   * Delete a task.
   *
   * @throws NS_ERROR_FILE_NOT_FOUND if the folder or task do not exist.
   *
   * @param aFolderName     Full name of the folder containing the task, starting with "\".
   * @param aTaskName       Name of the task to delete.
   */
  void deleteTask(in wstring aFolderName, in wstring aTaskName);

  /**
   * List the names of all tasks in a task folder.
   *
   * @throws NS_ERROR_FILE_NOT_FOUND if the folder doesn't exist.
   *
   * @param aFolderName     The full name of the task folder to enumerate, starting with "\".
   *
   * @return                An array with the names of the tasks found.
   */
  Array<AString> getFolderTasks(in wstring aFolderName);

  /**
   * Create a new task subfolder under a given parent folder.
   *
   * @throws NS_ERROR_FILE_NOT_FOUND if the parent folder does not exist.
   * @throws NS_ERROR_FILE_ALREADY_EXISTS if the subfolder already exists.
   *
   * @param aParentFolderName       Immediate parent for the new folder, starting with "\".
   * @param aSubFolderName          Name of the new folder to create.
   */
  void createFolder(in wstring aParentFolderName, in wstring aSubFolderName);

  /**
   * Delete a folder.
   *
   * @throws NS_ERROR_FILE_NOT_FOUND if the parent folder does not exist.
   * @throws NS_ERROR_FILE_DIR_NOT_EMPTY if the folder was not empty.
   *
   * @param aParentFolderName       Immediate parent of the folder to delete, starting with "\".
   * @param aSubFolderName          Name of the folder to delete.
   */
  void deleteFolder(in wstring aParentFolderName, in wstring aSubFolderName);
};
+308 −0
Original line number Diff line number Diff line
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode:nil; c-basic-offset: 2 -*- */
/* 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 "nsWinTaskScheduler.h"

#include <windows.h>
#include <comdef.h>
#include <taskschd.h>

#include "nsString.h"

#include "mozilla/RefPtr.h"
#include "mozilla/ResultVariant.h"

using namespace mozilla;

struct SysFreeStringDeleter {
  void operator()(BSTR aPtr) { ::SysFreeString(aPtr); }
};
using BStrPtr = mozilla::UniquePtr<OLECHAR, SysFreeStringDeleter>;

static nsresult ToNotFoundOrFailure(HRESULT hr) {
  if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) {
    return NS_ERROR_FILE_NOT_FOUND;
  } else {
    return NS_ERROR_FAILURE;
  }
}

static nsresult ToAlreadyExistsOrFailure(HRESULT hr) {
  if (hr == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) {
    return NS_ERROR_FILE_ALREADY_EXISTS;
  } else {
    return NS_ERROR_FAILURE;
  }
}

[[nodiscard]] static Result<RefPtr<ITaskFolder>, HRESULT> GetTaskFolder(
    const char16_t* aFolderName) {
  HRESULT hr;
  RefPtr<ITaskService> scheduler = nullptr;

  hr = CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
                        IID_ITaskService, getter_AddRefs(scheduler));
  if (FAILED(hr)) {
    return Err(hr);
  }

  // Connect to the local Task Scheduler.
  hr = scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{});
  if (FAILED(hr)) {
    return Err(hr);
  }

  BStrPtr bstrFolderName =
      BStrPtr(::SysAllocString(reinterpret_cast<const OLECHAR*>(aFolderName)));

  RefPtr<ITaskFolder> folder = nullptr;
  hr = scheduler->GetFolder(bstrFolderName.get(), getter_AddRefs(folder));
  if (FAILED(hr)) {
    return Err(hr);
  }

  return folder;
}

[[nodiscard]] static Result<RefPtr<IRegisteredTask>, HRESULT> GetRegisteredTask(
    const char16_t* aFolderName, const char16_t* aTaskName) {
  auto folder = GetTaskFolder(aFolderName);
  if (!folder.isOk()) {
    return Err(folder.unwrapErr());
  }

  BStrPtr bstrTaskName =
      BStrPtr(::SysAllocString(reinterpret_cast<const OLECHAR*>(aTaskName)));

  RefPtr<IRegisteredTask> task = nullptr;
  HRESULT hr =
      folder.unwrap()->GetTask(bstrTaskName.get(), getter_AddRefs(task));
  if (FAILED(hr)) {
    return Err(hr);
  }

  return task;
}

NS_IMPL_ISUPPORTS(nsWinTaskSchedulerService, nsIWinTaskSchedulerService)

NS_IMETHODIMP
nsWinTaskSchedulerService::GetTaskXML(const char16_t* aFolderName,
                                      const char16_t* aTaskName,
                                      nsAString& aResult) {
  if (!aFolderName || !aTaskName) {
    return NS_ERROR_NULL_POINTER;
  }

  auto task = GetRegisteredTask(aFolderName, aTaskName);
  if (!task.isOk()) {
    return ToNotFoundOrFailure(task.unwrapErr());
  }

  {
    BSTR bstrXml = nullptr;
    if (FAILED(task.unwrap()->get_Xml(&bstrXml))) {
      return NS_ERROR_FAILURE;
    }

    aResult.Assign(bstrXml, ::SysStringLen(bstrXml));
    ::SysFreeString(bstrXml);
  }

  return NS_OK;
}

NS_IMETHODIMP
nsWinTaskSchedulerService::RegisterTask(const char16_t* aFolderName,
                                        const char16_t* aTaskName,
                                        const char16_t* aDefinitionXML,
                                        bool aUpdateExisting) {
  if (!aFolderName || !aTaskName || !aDefinitionXML) {
    return NS_ERROR_NULL_POINTER;
  }

  auto folder = GetTaskFolder(aFolderName);
  if (!folder.isOk()) {
    return ToNotFoundOrFailure(folder.unwrapErr());
  }

  BStrPtr bstrTaskName =
      BStrPtr(::SysAllocString(reinterpret_cast<const OLECHAR*>(aTaskName)));
  BStrPtr bstrXml = BStrPtr(
      ::SysAllocString(reinterpret_cast<const OLECHAR*>(aDefinitionXML)));
  LONG flags = aUpdateExisting ? TASK_CREATE_OR_UPDATE : TASK_CREATE;
  TASK_LOGON_TYPE logonType = TASK_LOGON_INTERACTIVE_TOKEN;

  // The outparam is not needed, but not documented as optional.
  RefPtr<IRegisteredTask> unusedTaskOutput = nullptr;
  HRESULT hr = folder.unwrap()->RegisterTask(
      bstrTaskName.get(), bstrXml.get(), flags, VARIANT{} /* userId */,
      VARIANT{} /* password */, logonType, VARIANT{} /* sddl */,
      getter_AddRefs(unusedTaskOutput));

  if (FAILED(hr)) {
    return ToAlreadyExistsOrFailure(hr);
  }

  return NS_OK;
}

NS_IMETHODIMP
nsWinTaskSchedulerService::ValidateTaskDefinition(
    const char16_t* aDefinitionXML, int32_t* aResult) {
  if (!aDefinitionXML) {
    return NS_ERROR_NULL_POINTER;
  }

  auto folder = GetTaskFolder(reinterpret_cast<const char16_t*>(L"\\"));
  if (!folder.isOk()) {
    return NS_ERROR_FAILURE;
  }

  BStrPtr bstrXml = BStrPtr(
      ::SysAllocString(reinterpret_cast<const OLECHAR*>(aDefinitionXML)));
  LONG flags = TASK_VALIDATE_ONLY;
  TASK_LOGON_TYPE logonType = TASK_LOGON_INTERACTIVE_TOKEN;

  // The outparam is not needed, but not documented as optional.
  RefPtr<IRegisteredTask> unusedTaskOutput = nullptr;
  HRESULT hr = folder.unwrap()->RegisterTask(
      nullptr /* path */, bstrXml.get(), flags, VARIANT{} /* userId */,
      VARIANT{} /* password */, logonType, VARIANT{} /* sddl */,
      getter_AddRefs(unusedTaskOutput));

  if (aResult) {
    *aResult = hr;
  }

  return NS_OK;
}

NS_IMETHODIMP
nsWinTaskSchedulerService::DeleteTask(const char16_t* aFolderName,
                                      const char16_t* aTaskName) {
  if (!aFolderName || !aTaskName) {
    return NS_ERROR_NULL_POINTER;
  }

  auto folder = GetTaskFolder(aFolderName);
  if (!folder.isOk()) {
    return ToNotFoundOrFailure(folder.unwrapErr());
  }

  BStrPtr bstrTaskName =
      BStrPtr(::SysAllocString(reinterpret_cast<const OLECHAR*>(aTaskName)));

  HRESULT hr = folder.unwrap()->DeleteTask(bstrTaskName.get(), 0 /* flags */);
  if (FAILED(hr)) {
    return ToNotFoundOrFailure(hr);
  }

  return NS_OK;
}

NS_IMETHODIMP
nsWinTaskSchedulerService::GetFolderTasks(const char16_t* aFolderName,
                                          nsTArray<nsString>& aResult) {
  if (!aFolderName) {
    return NS_ERROR_NULL_POINTER;
  }

  auto folder = GetTaskFolder(aFolderName);
  if (!folder.isOk()) {
    return ToNotFoundOrFailure(folder.unwrapErr());
  }

  RefPtr<IRegisteredTaskCollection> taskCollection = nullptr;
  if (FAILED(folder.unwrap()->GetTasks(TASK_ENUM_HIDDEN,
                                       getter_AddRefs(taskCollection)))) {
    return NS_ERROR_FAILURE;
  }

  LONG taskCount = 0;
  if (FAILED(taskCollection->get_Count(&taskCount))) {
    return NS_ERROR_FAILURE;
  }

  aResult.Clear();

  for (LONG i = 0; i < taskCount; ++i) {
    RefPtr<IRegisteredTask> task = nullptr;

    // nb: Collections are indexed from 1.
    if (FAILED(taskCollection->get_Item(_variant_t(i + 1),
                                        getter_AddRefs(task)))) {
      return NS_ERROR_FAILURE;
    }

    BStrPtr bstrTaskName;
    {
      BSTR tempTaskName = nullptr;
      if (FAILED(task->get_Name(&tempTaskName))) {
        return NS_ERROR_FAILURE;
      }
      bstrTaskName = BStrPtr(tempTaskName);
    }

    aResult.AppendElement(nsString(bstrTaskName.get()));
  }

  return NS_OK;
}

NS_IMETHODIMP
nsWinTaskSchedulerService::CreateFolder(const char16_t* aParentFolderName,
                                        const char16_t* aSubFolderName) {
  if (!aParentFolderName || !aSubFolderName) {
    return NS_ERROR_NULL_POINTER;
  }

  auto parentFolder = GetTaskFolder(aParentFolderName);
  if (!parentFolder.isOk()) {
    return ToNotFoundOrFailure(parentFolder.unwrapErr());
  }

  BStrPtr bstrSubFolderName = BStrPtr(
      ::SysAllocString(reinterpret_cast<const OLECHAR*>(aSubFolderName)));

  HRESULT hr = parentFolder.unwrap()->CreateFolder(bstrSubFolderName.get(),
                                                   VARIANT{},  // sddl
                                                   nullptr);   // ppFolder

  if (FAILED(hr)) {
    return ToAlreadyExistsOrFailure(hr);
  }

  return NS_OK;
}

NS_IMETHODIMP
nsWinTaskSchedulerService::DeleteFolder(const char16_t* aParentFolderName,
                                        const char16_t* aSubFolderName) {
  if (!aParentFolderName || !aSubFolderName) {
    return NS_ERROR_NULL_POINTER;
  }

  auto parentFolder = GetTaskFolder(aParentFolderName);
  if (!parentFolder.isOk()) {
    return ToNotFoundOrFailure(parentFolder.unwrapErr());
  }

  BStrPtr bstrSubFolderName = BStrPtr(
      ::SysAllocString(reinterpret_cast<const OLECHAR*>(aSubFolderName)));

  HRESULT hr = parentFolder.unwrap()->DeleteFolder(bstrSubFolderName.get(),
                                                   0 /* flags */);

  if (FAILED(hr)) {
    if (hr == HRESULT_FROM_WIN32(ERROR_DIR_NOT_EMPTY)) {
      return NS_ERROR_FILE_DIR_NOT_EMPTY;
    } else {
      return ToNotFoundOrFailure(hr);
    }
  }

  return NS_OK;
}
Loading