summaryrefslogtreecommitdiffstats
path: root/toolkit/components/taskscheduler
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/taskscheduler
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/taskscheduler')
-rw-r--r--toolkit/components/taskscheduler/TaskScheduler.jsm124
-rw-r--r--toolkit/components/taskscheduler/TaskSchedulerWinImpl.jsm229
-rw-r--r--toolkit/components/taskscheduler/components.conf18
-rw-r--r--toolkit/components/taskscheduler/moz.build26
-rw-r--r--toolkit/components/taskscheduler/nsIWinTaskSchedulerService.idl104
-rw-r--r--toolkit/components/taskscheduler/nsWinTaskScheduler.cpp308
-rw-r--r--toolkit/components/taskscheduler/nsWinTaskScheduler.h22
-rw-r--r--toolkit/components/taskscheduler/tests/xpcshell/test_TaskScheduler.js40
-rw-r--r--toolkit/components/taskscheduler/tests/xpcshell/test_TaskSchedulerWinImpl.js205
-rw-r--r--toolkit/components/taskscheduler/tests/xpcshell/test_WinTaskSchedulerService.js191
-rw-r--r--toolkit/components/taskscheduler/tests/xpcshell/xpcshell.ini5
11 files changed, 1272 insertions, 0 deletions
diff --git a/toolkit/components/taskscheduler/TaskScheduler.jsm b/toolkit/components/taskscheduler/TaskScheduler.jsm
new file mode 100644
index 0000000000..5b3e79d8ac
--- /dev/null
+++ b/toolkit/components/taskscheduler/TaskScheduler.jsm
@@ -0,0 +1,124 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TaskScheduler"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetter(
+ this,
+ "WinImpl",
+ "resource://gre/modules/TaskSchedulerWinImpl.jsm",
+ "_TaskSchedulerWinImpl"
+);
+
+XPCOMUtils.defineLazyGetter(this, "gImpl", () => {
+ if (AppConstants.platform == "win") {
+ return WinImpl;
+ }
+
+ // Stubs for unsupported platforms
+ return {
+ registerTask() {},
+ deleteTask() {},
+ deleteAllTasks() {},
+ };
+});
+
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+/**
+ * Interface to a system task scheduler, capable of running a command line at an interval
+ * independent of the application.
+ *
+ * Currently only implemented for Windows, on other platforms these calls do nothing.
+ *
+ * The implementation will only interact with tasks from the same install of this application.
+ * - On Windows the native tasks are named like "\<vendor>\<id> <install path hash>",
+ * e.g. "\Mozilla\Task Identifier 308046B0AF4A39CB"
+ */
+var TaskScheduler = {
+ MIN_INTERVAL_SECONDS: 1800,
+
+ /**
+ * Create a scheduled task that will run a command indepedent of the application.
+ *
+ * It will run every intervalSeconds seconds, starting intervalSeconds seconds from now.
+ *
+ * If the task is unable to run one or more scheduled times (e.g. if the computer is
+ * off, or the owning user is not logged in), then the next time a run is possible the task
+ * will be run once.
+ *
+ * An existing task with the same `id` will be replaced.
+ *
+ * Only one instance of the task will run at once, though this does not affect different
+ * tasks from the same application.
+ *
+ * @param id
+ * A unique string (including a UUID is recommended) to distinguish the task among
+ * other tasks from this installation.
+ * This string will also be visible to system administrators, so it should be a legible
+ * description, but it does not need to be localized.
+ *
+ * @param command
+ * Full path to the executable to run.
+ *
+ * @param intervalSeconds
+ * Interval at which to run the command, in seconds. Minimum 1800 (30 minutes).
+ *
+ * @param options
+ * Optional, as as all of its properties:
+ * {
+ * args
+ * Array of arguments to pass on the command line. Does not include the command
+ * itself even if that is considered part of the command line. If missing, no
+ * argument list is generated.
+ *
+ * workingDirectory
+ * Working directory for the command. If missing, no working directory is set.
+ *
+ * description
+ * A description string that will be visible to system administrators. This should
+ * be localized. If missing, no description is set.
+ *
+ * disabled
+ * If true the task will be created disabled, so that it will not be run.
+ * Default false, intended for tests.
+ * }
+ * }
+ */
+ registerTask(id, command, intervalSeconds, options) {
+ if (!Number.isInteger(intervalSeconds)) {
+ throw new Error("Interval is not an integer");
+ }
+ if (intervalSeconds < this.MIN_INTERVAL_SECONDS) {
+ throw new Error("Interval is too short");
+ }
+
+ return gImpl.registerTask(id, command, intervalSeconds, options);
+ },
+
+ /**
+ * Delete a scheduled task previously created with registerTask.
+ *
+ * @throws NS_ERROR_FILE_NOT_FOUND if the task does not exist.
+ */
+ deleteTask(id) {
+ return gImpl.deleteTask(id);
+ },
+
+ /**
+ * Delete all tasks registered by this application.
+ */
+ deleteAllTasks() {
+ return gImpl.deleteAllTasks();
+ },
+};
diff --git a/toolkit/components/taskscheduler/TaskSchedulerWinImpl.jsm b/toolkit/components/taskscheduler/TaskSchedulerWinImpl.jsm
new file mode 100644
index 0000000000..ca4ebaa73e
--- /dev/null
+++ b/toolkit/components/taskscheduler/TaskSchedulerWinImpl.jsm
@@ -0,0 +1,229 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["_TaskSchedulerWinImpl"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetters(this, {
+ WinTaskSvc: [
+ "@mozilla.org/win-task-scheduler-service;1",
+ "nsIWinTaskSchedulerService",
+ ],
+ XreDirProvider: [
+ "@mozilla.org/xre/directory-provider;1",
+ "nsIXREDirProvider",
+ ],
+});
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["XMLSerializer"]);
+
+/**
+ * Task generation and management for Windows, using Task Scheduler 2.0 (taskschd).
+ *
+ * Implements the API exposed in TaskScheduler.jsm
+ * Not intended for external use, this is in a separate module to ship the code only
+ * on Windows, and to expose for testing.
+ */
+var _TaskSchedulerWinImpl = {
+ registerTask(id, command, intervalSeconds, options) {
+ // The folder might not yet exist.
+ this._createFolderIfNonexistent();
+
+ const xml = this._formatTaskDefinitionXML(
+ command,
+ intervalSeconds,
+ options
+ );
+ const updateExisting = true;
+
+ WinTaskSvc.registerTask(
+ this._taskFolderName(),
+ this._formatTaskName(id),
+ xml,
+ updateExisting
+ );
+ },
+
+ deleteTask(id) {
+ WinTaskSvc.deleteTask(this._taskFolderName(), this._formatTaskName(id));
+ },
+
+ deleteAllTasks() {
+ const taskFolderName = this._taskFolderName();
+
+ let allTasks;
+ try {
+ allTasks = WinTaskSvc.getFolderTasks(taskFolderName);
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
+ // Folder doesn't exist, nothing to delete.
+ return;
+ }
+ throw ex;
+ }
+
+ const tasksToDelete = allTasks.filter(name => this._matchAppTaskName(name));
+
+ for (const taskName of tasksToDelete) {
+ WinTaskSvc.deleteTask(taskFolderName, taskName);
+ }
+
+ if (allTasks.length == tasksToDelete.length) {
+ // Deleted every task, remove the folder.
+ this._deleteFolderIfEmpty();
+ }
+ },
+
+ _formatTaskDefinitionXML(command, intervalSeconds, options) {
+ const startTime = new Date(Date.now() + intervalSeconds * 1000);
+ const xmlns = "http://schemas.microsoft.com/windows/2004/02/mit/task";
+
+ // Fill in the constant parts of the task, and those that don't require escaping.
+ const docBase = `<Task xmlns="${xmlns}">
+ <Triggers>
+ <TimeTrigger>
+ <StartBoundary>${startTime.toISOString()}</StartBoundary>
+ <Repetition>
+ <Interval>PT${intervalSeconds}S</Interval>
+ </Repetition>
+ </TimeTrigger>
+ </Triggers>
+ <Actions>
+ <Exec />
+ </Actions>
+ <Settings>
+ <StartWhenAvailable>true</StartWhenAvailable>
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
+ </Settings>
+ <RegistrationInfo>
+ <Author />
+ </RegistrationInfo>
+</Task>`;
+ const doc = new DOMParser().parseFromString(docBase, "text/xml");
+
+ const execAction = doc.querySelector("Actions Exec");
+
+ const commandNode = doc.createElementNS(xmlns, "Command");
+ commandNode.textContent = command;
+ execAction.appendChild(commandNode);
+
+ if (options?.args) {
+ const args = doc.createElementNS(xmlns, "Arguments");
+ args.textContent = options.args.map(this._quoteString).join(" ");
+ execAction.appendChild(args);
+ }
+
+ if (options?.workingDirectory) {
+ const workingDirectory = doc.createElementNS(xmlns, "WorkingDirectory");
+ workingDirectory.textContent = options.workingDirectory;
+ execAction.appendChild(workingDirectory);
+ }
+
+ if (options?.disabled) {
+ const enabled = doc.createElementNS(xmlns, "Enabled");
+ enabled.textContent = "false";
+ doc.querySelector("Settings").appendChild(enabled);
+ }
+
+ // Other settings to consider for the future:
+ // Idle
+ // Battery
+ // Max run time
+
+ doc.querySelector("RegistrationInfo Author").textContent =
+ Services.appinfo.vendor;
+
+ if (options?.description) {
+ const registrationInfo = doc.querySelector("RegistrationInfo");
+ const description = doc.createElementNS(xmlns, "Description");
+ description.textContent = options.description;
+ registrationInfo.appendChild(description);
+ }
+
+ const serializer = new XMLSerializer();
+ return serializer.serializeToString(doc);
+ },
+
+ _createFolderIfNonexistent() {
+ const { parentName, subName } = this._taskFolderNameParts();
+
+ try {
+ WinTaskSvc.createFolder(parentName, subName);
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
+ throw e;
+ }
+ }
+ },
+
+ _deleteFolderIfEmpty() {
+ const { parentName, subName } = this._taskFolderNameParts();
+
+ try {
+ WinTaskSvc.deleteFolder(parentName, subName);
+ } catch (e) {
+ // Missed one somehow, possibly a subfolder?
+ if (e.result != Cr.NS_ERROR_FILE_DIR_NOT_EMPTY) {
+ throw e;
+ }
+ }
+ },
+
+ /**
+ * Quotes a string for use as a single command argument, using Windows quoting
+ * conventions.
+ *
+ * copied from quoteString() in toolkit/modules/subproces/subprocess_worker_win.js
+ *
+ *
+ * @see https://msdn.microsoft.com/en-us/library/17w5ykft(v=vs.85).aspx
+ *
+ * @param {string} str
+ * The argument string to quote.
+ * @returns {string}
+ */
+ _quoteString(str) {
+ if (!/[\s"]/.test(str)) {
+ return str;
+ }
+
+ let escaped = str.replace(/(\\*)("|$)/g, (m0, m1, m2) => {
+ if (m2) {
+ m2 = `\\${m2}`;
+ }
+ return `${m1}${m1}${m2}`;
+ });
+
+ return `"${escaped}"`;
+ },
+
+ _taskFolderName() {
+ return `\\${Services.appinfo.vendor}`;
+ },
+
+ _taskFolderNameParts() {
+ return {
+ parentName: "\\",
+ subName: Services.appinfo.vendor,
+ };
+ },
+
+ _formatTaskName(id) {
+ const installHash = XreDirProvider.getInstallHash();
+ return `${id} ${installHash}`;
+ },
+
+ _matchAppTaskName(name) {
+ const installHash = XreDirProvider.getInstallHash();
+ return name.endsWith(` ${installHash}`);
+ },
+};
diff --git a/toolkit/components/taskscheduler/components.conf b/toolkit/components/taskscheduler/components.conf
new file mode 100644
index 0000000000..c6e96da1b4
--- /dev/null
+++ b/toolkit/components/taskscheduler/components.conf
@@ -0,0 +1,18 @@
+# -*- 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,
+ },
+ ]
diff --git a/toolkit/components/taskscheduler/moz.build b/toolkit/components/taskscheduler/moz.build
new file mode 100644
index 0000000000..50b22b3a66
--- /dev/null
+++ b/toolkit/components/taskscheduler/moz.build
@@ -0,0 +1,26 @@
+# -*- 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"]
+
+EXTRA_JS_MODULES += ["TaskScheduler.jsm"]
+
+# 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":
+ EXTRA_JS_MODULES += ["TaskSchedulerWinImpl.jsm"]
+ XPIDL_SOURCES += ["nsIWinTaskSchedulerService.idl"]
+ EXPORTS += ["nsWinTaskScheduler.h"]
+ SOURCES += ["nsWinTaskScheduler.cpp"]
+ OS_LIBS += ["taskschd"]
+
+FINAL_LIBRARY = "xul"
diff --git a/toolkit/components/taskscheduler/nsIWinTaskSchedulerService.idl b/toolkit/components/taskscheduler/nsIWinTaskSchedulerService.idl
new file mode 100644
index 0000000000..7a7c5747c0
--- /dev/null
+++ b/toolkit/components/taskscheduler/nsIWinTaskSchedulerService.idl
@@ -0,0 +1,104 @@
+/* -*- 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);
+};
diff --git a/toolkit/components/taskscheduler/nsWinTaskScheduler.cpp b/toolkit/components/taskscheduler/nsWinTaskScheduler.cpp
new file mode 100644
index 0000000000..21b2909805
--- /dev/null
+++ b/toolkit/components/taskscheduler/nsWinTaskScheduler.cpp
@@ -0,0 +1,308 @@
+/* -*- 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;
+}
diff --git a/toolkit/components/taskscheduler/nsWinTaskScheduler.h b/toolkit/components/taskscheduler/nsWinTaskScheduler.h
new file mode 100644
index 0000000000..e89c1b9b05
--- /dev/null
+++ b/toolkit/components/taskscheduler/nsWinTaskScheduler.h
@@ -0,0 +1,22 @@
+/* -*- 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/. */
+
+#ifndef WINTASKSCHEDULER_H_
+#define WINTASKSCHEDULER_H_
+
+#include "nsIWinTaskSchedulerService.h"
+
+class nsWinTaskSchedulerService : public nsIWinTaskSchedulerService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIWINTASKSCHEDULERSERVICE
+
+ nsWinTaskSchedulerService() = default;
+
+ protected:
+ virtual ~nsWinTaskSchedulerService() = default;
+};
+
+#endif /* WINTASKSCHEDULER_H_ */
diff --git a/toolkit/components/taskscheduler/tests/xpcshell/test_TaskScheduler.js b/toolkit/components/taskscheduler/tests/xpcshell/test_TaskScheduler.js
new file mode 100644
index 0000000000..47e641530b
--- /dev/null
+++ b/toolkit/components/taskscheduler/tests/xpcshell/test_TaskScheduler.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+// Cross-platform task scheduler tests.
+//
+// There's not much that can be done here without allowing the task to run, so this
+// only touches on the basics of argument checking. On platforms without a task
+// scheduler implementation, these interfaces currently do nothing else.
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { updateAppInfo } = ChromeUtils.import(
+ "resource://testing-common/AppInfo.jsm"
+);
+updateAppInfo();
+
+const { TaskScheduler } = ChromeUtils.import(
+ "resource://gre/modules/TaskScheduler.jsm"
+);
+
+registerCleanupFunction(() => {
+ TaskScheduler.deleteAllTasks();
+});
+
+add_task(async function test_gen() {
+ TaskScheduler.registerTask("FOO", "xyz", TaskScheduler.MIN_INTERVAL_SECONDS, {
+ disabled: true,
+ });
+ TaskScheduler.deleteTask("FOO");
+
+ Assert.throws(
+ () =>
+ TaskScheduler.registerTask("BAR", "123", 1, {
+ disabled: true,
+ }),
+ /Interval is too short/
+ );
+});
diff --git a/toolkit/components/taskscheduler/tests/xpcshell/test_TaskSchedulerWinImpl.js b/toolkit/components/taskscheduler/tests/xpcshell/test_TaskSchedulerWinImpl.js
new file mode 100644
index 0000000000..19c6a86abc
--- /dev/null
+++ b/toolkit/components/taskscheduler/tests/xpcshell/test_TaskSchedulerWinImpl.js
@@ -0,0 +1,205 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+// Unit tests for Windows scheduled task generation.
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { updateAppInfo } = ChromeUtils.import(
+ "resource://testing-common/AppInfo.jsm"
+);
+updateAppInfo();
+
+const { TaskScheduler } = ChromeUtils.import(
+ "resource://gre/modules/TaskScheduler.jsm"
+);
+
+const { _TaskSchedulerWinImpl: WinImpl } = ChromeUtils.import(
+ "resource://gre/modules/TaskSchedulerWinImpl.jsm"
+);
+
+const WinSvc = Cc["@mozilla.org/win-task-scheduler-service;1"].getService(
+ Ci.nsIWinTaskSchedulerService
+);
+
+const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(
+ Ci.nsIUUIDGenerator
+);
+
+function randomName() {
+ return (
+ "moz-taskschd-test-" +
+ uuidGenerator
+ .generateUUID()
+ .toString()
+ .slice(1, -1)
+ );
+}
+
+const gFolderName = randomName();
+
+// Override task folder name, to prevent colliding with other tests.
+WinImpl._taskFolderName = function() {
+ return gFolderName;
+};
+WinImpl._taskFolderNameParts = function() {
+ return {
+ parentName: "\\",
+ subName: gFolderName,
+ };
+};
+
+registerCleanupFunction(() => {
+ TaskScheduler.deleteAllTasks();
+});
+
+add_task(function test_create() {
+ const taskName = "test-task-1";
+ const rawTaskName = WinImpl._formatTaskName(taskName);
+ const folderName = WinImpl._taskFolderName();
+ const exePath = "C:\\Program Files\\XYZ\\123.exe";
+ const workingDir = "C:\\Program Files\\XYZ";
+ const argsIn = [
+ "x.txt",
+ "c:\\x.txt",
+ 'C:\\"HELLO WORLD".txt',
+ "only space.txt",
+ ];
+ const expectedArgsOutStr = [
+ "x.txt",
+ "c:\\x.txt",
+ '"C:\\\\\\"HELLO WORLD\\".txt"',
+ '"only space.txt"',
+ ].join(" ");
+ const description = "Entities: < &. Non-ASCII: abc😀def.";
+ const intervalSecsIn = 2 * 60 * 60; // 2 hours
+ const expectedIntervalOutWin10 = "PT2H"; // Windows 10 regroups by hours and minutes
+ const expectedIntervalOutWin7 = `PT${intervalSecsIn}S`; // Windows 7 doesn't regroup
+
+ TaskScheduler.registerTask(taskName, exePath, intervalSecsIn, {
+ disabled: true,
+ args: argsIn,
+ description,
+ workingDirectory: workingDir,
+ });
+
+ // Read back the task
+ const readBackXML = WinSvc.getTaskXML(folderName, rawTaskName);
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(readBackXML, "text/xml");
+ Assert.equal(doc.documentElement.tagName, "Task");
+
+ // Check for the values set above
+ Assert.equal(doc.querySelector("Actions Exec Command").textContent, exePath);
+ Assert.equal(
+ doc.querySelector("Actions Exec WorkingDirectory").textContent,
+ workingDir
+ );
+ Assert.equal(
+ doc.querySelector("Actions Exec Arguments").textContent,
+ expectedArgsOutStr
+ );
+ Assert.equal(
+ doc.querySelector("RegistrationInfo Description").textContent,
+ description
+ );
+ Assert.equal(
+ doc.querySelector("RegistrationInfo Author").textContent,
+ Services.appinfo.vendor
+ );
+
+ Assert.equal(doc.querySelector("Settings Enabled").textContent, "false");
+
+ // Note: It's a little too tricky to check for a specific StartBoundary value reliably here, given
+ // that it gets set relative to Date.now(), so I'm skipping that.
+ const intervalOut = doc.querySelector(
+ "Triggers TimeTrigger Repetition Interval"
+ ).textContent;
+ Assert.ok(
+ intervalOut == expectedIntervalOutWin7 ||
+ intervalOut == expectedIntervalOutWin10
+ );
+
+ // Validate the XML
+ WinSvc.validateTaskDefinition(readBackXML);
+
+ // Update
+ const updatedExePath = "C:\\Program Files (x86)\\ABC\\foo.exe";
+ const updatedIntervalSecsIn = 3 * 60 * 60; // 3 hours
+ const expectedUpdatedIntervalOutWin10 = "PT3H";
+ const expectedUpdatedIntervalOutWin7 = `PT${updatedIntervalSecsIn}S`;
+
+ TaskScheduler.registerTask(taskName, updatedExePath, updatedIntervalSecsIn, {
+ disabled: true,
+ args: argsIn,
+ description,
+ workingDirectory: workingDir,
+ });
+
+ // Read back the updated task
+ const readBackUpdatedXML = WinSvc.getTaskXML(folderName, rawTaskName);
+ const updatedDoc = parser.parseFromString(readBackUpdatedXML, "text/xml");
+ Assert.equal(updatedDoc.documentElement.tagName, "Task");
+
+ // Check for updated values
+ Assert.equal(
+ updatedDoc.querySelector("Actions Exec Command").textContent,
+ updatedExePath
+ );
+
+ Assert.notEqual(
+ doc.querySelector("Triggers TimeTrigger StartBoundary").textContent,
+ updatedDoc.querySelector("Triggers TimeTrigger StartBoundary").textContent
+ );
+ const updatedIntervalOut = updatedDoc.querySelector(
+ "Triggers TimeTrigger Repetition Interval"
+ ).textContent;
+ Assert.ok(
+ updatedIntervalOut == expectedUpdatedIntervalOutWin7 ||
+ updatedIntervalOut == expectedUpdatedIntervalOutWin10
+ );
+
+ // Check that the folder really was there
+ {
+ const { parentName, subName } = WinImpl._taskFolderNameParts();
+ let threw;
+ try {
+ WinSvc.deleteFolder(parentName, subName);
+ } catch (ex) {
+ threw = ex;
+ }
+ Assert.equal(threw.result, Cr.NS_ERROR_FILE_DIR_NOT_EMPTY);
+ }
+
+ // Delete
+ TaskScheduler.deleteAllTasks();
+
+ // Check that the folder is gone
+ {
+ const { parentName, subName } = WinImpl._taskFolderNameParts();
+ let threw;
+ try {
+ WinSvc.deleteFolder(parentName, subName);
+ } catch (ex) {
+ threw = ex;
+ }
+ Assert.equal(threw.result, Cr.NS_ERROR_FILE_NOT_FOUND);
+ }
+
+ // Format and validate the XML with the task not disabled
+ const enabledXML = WinImpl._formatTaskDefinitionXML(exePath, intervalSecsIn, {
+ args: argsIn,
+ description,
+ workingDirectory: workingDir,
+ });
+ Assert.equal(WinSvc.validateTaskDefinition(enabledXML), 0 /* S_OK */);
+
+ // Format and validate with no options
+ const basicXML = WinImpl._formatTaskDefinitionXML(
+ "foo",
+ TaskScheduler.MIN_INTERVAL_SECONDS
+ );
+ Assert.equal(WinSvc.validateTaskDefinition(basicXML), 0 /* S_OK */);
+});
diff --git a/toolkit/components/taskscheduler/tests/xpcshell/test_WinTaskSchedulerService.js b/toolkit/components/taskscheduler/tests/xpcshell/test_WinTaskSchedulerService.js
new file mode 100644
index 0000000000..5ffe018573
--- /dev/null
+++ b/toolkit/components/taskscheduler/tests/xpcshell/test_WinTaskSchedulerService.js
@@ -0,0 +1,191 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+// Unit tests for access to the Windows Task Scheduler via nsIWinTaskSchedulerService.
+
+const svc = Cc["@mozilla.org/win-task-scheduler-service;1"].getService(
+ Ci.nsIWinTaskSchedulerService
+);
+
+const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(
+ Ci.nsIUUIDGenerator
+);
+
+function randomName() {
+ return (
+ "moz-taskschd-test-" +
+ uuidGenerator
+ .generateUUID()
+ .toString()
+ .slice(1, -1)
+ );
+}
+
+const gParentFolderName = randomName();
+const gParentFolderPath = `\\${gParentFolderName}`;
+const gSubFolderName = randomName();
+const gSubFolderPath = `\\${gParentFolderName}\\${gSubFolderName}`;
+// This folder will not be created
+const gMissingFolderName = randomName();
+const gMissingFolderPath = `\\${gParentFolderName}\\${gMissingFolderName}`;
+
+const gValidTaskXML = `<Task xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
+ <Triggers />
+ <Settings>
+ <Enabled>false</Enabled>
+ </Settings>
+ <Actions>
+ <Exec>
+ <Command>xyz123.exe</Command>
+ </Exec>
+ </Actions>
+</Task>`;
+
+// Missing actions
+const gInvalidTaskXML = `<Task xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
+ <Triggers />
+ <Settings>
+ <Enabled>false</Enabled>
+ </Settings>
+</Task>`;
+
+function cleanup() {
+ let tasksToDelete = svc.getFolderTasks(gSubFolderPath);
+
+ for (const task of tasksToDelete) {
+ svc.deleteTask(gSubFolderPath, task);
+ }
+
+ svc.deleteFolder(gParentFolderPath, gSubFolderName);
+
+ svc.deleteFolder("\\", gParentFolderPath);
+}
+
+registerCleanupFunction(() => {
+ try {
+ cleanup();
+ } catch (_ex) {
+ // Folders may not exist
+ }
+});
+
+add_task(async function test_svc() {
+ /***** FOLDERS *****/
+
+ // Try creating subfolder before parent folder exists
+ Assert.throws(
+ () => svc.createFolder(gParentFolderPath, gSubFolderName),
+ /NS_ERROR_FILE_NOT_FOUND/
+ );
+
+ // Create parent folder
+ svc.createFolder("\\", gParentFolderName);
+
+ // Create subfolder
+ svc.createFolder(gParentFolderPath, gSubFolderName);
+
+ // Try creating existing folder
+ Assert.throws(
+ () => svc.createFolder(gParentFolderPath, gSubFolderName),
+ /NS_ERROR_FILE_ALREADY_EXISTS/
+ );
+
+ // Try deleting nonexistent subfolder
+ Assert.throws(
+ () => svc.deleteFolder(gParentFolderPath, gMissingFolderName),
+ /NS_ERROR_FILE_NOT_FOUND/
+ );
+
+ /***** TASKS *****/
+ const taskNames = [randomName(), randomName(), randomName()];
+
+ // Try enumerating nonexistent subfolder
+ Assert.throws(
+ () => svc.getFolderTasks(gMissingFolderPath),
+ /NS_ERROR_FILE_NOT_FOUND/
+ );
+
+ // List empty subfolder
+ Assert.deepEqual(svc.getFolderTasks(gSubFolderPath), []);
+
+ // Try to create task in nonexistent subfolder
+ Assert.throws(
+ () => svc.registerTask(gMissingFolderPath, taskNames[0], gValidTaskXML),
+ /NS_ERROR_FILE_NOT_FOUND/
+ );
+
+ // Create task 0
+
+ svc.registerTask(gSubFolderPath, taskNames[0], gValidTaskXML);
+
+ // Try to recreate task 0
+ Assert.throws(
+ () => svc.registerTask(gSubFolderPath, taskNames[0], gValidTaskXML),
+ /NS_ERROR_FILE_ALREADY_EXISTS/
+ );
+
+ // Update task 0
+ svc.registerTask(
+ gSubFolderPath,
+ taskNames[0],
+ gValidTaskXML,
+ true /* aUpdateExisting */
+ );
+
+ // Read back XML
+ Assert.ok(svc.getTaskXML(gSubFolderPath, taskNames[0]));
+
+ // Create remaining tasks
+ for (const task of taskNames.slice(1)) {
+ svc.registerTask(gSubFolderPath, task, gValidTaskXML);
+ }
+
+ // Try to create with invalid XML
+ Assert.throws(
+ () => svc.registerTask(gSubFolderPath, randomName(), gInvalidTaskXML),
+ /NS_ERROR_FAILURE/
+ );
+
+ // Validate XML
+ Assert.equal(svc.validateTaskDefinition(gValidTaskXML), 0 /* S_OK */);
+
+ // Try to validate invalid XML
+ Assert.notEqual(svc.validateTaskDefinition(gInvalidTaskXML), 0 /* S_OK */);
+
+ // Test enumeration
+ {
+ let foundTasks = svc.getFolderTasks(gSubFolderPath);
+ foundTasks.sort();
+
+ let allTasks = taskNames.slice();
+ allTasks.sort();
+
+ Assert.deepEqual(foundTasks, allTasks);
+ }
+
+ // Try deleting non-empty folder
+ Assert.throws(
+ () => svc.deleteFolder(gParentFolderPath, gSubFolderName),
+ /NS_ERROR_FILE_DIR_NOT_EMPTY/
+ );
+
+ const missingTaskName = randomName();
+
+ // Try deleting non-existent task
+ Assert.throws(
+ () => svc.deleteTask(gSubFolderName, missingTaskName),
+ /NS_ERROR_FILE_NOT_FOUND/
+ );
+
+ // Try reading non-existent task
+ Assert.throws(
+ () => svc.getTaskXML(gSubFolderPath, missingTaskName),
+ /NS_ERROR_FILE_NOT_FOUND/
+ );
+
+ /***** Cleanup *****/
+ // Explicitly call cleanup() to test that it removes the folder without error.
+ cleanup();
+});
diff --git a/toolkit/components/taskscheduler/tests/xpcshell/xpcshell.ini b/toolkit/components/taskscheduler/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..ee406fe74b
--- /dev/null
+++ b/toolkit/components/taskscheduler/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,5 @@
+[test_WinTaskSchedulerService.js]
+run-if = os == "win" # Test of Windows only service
+[test_TaskSchedulerWinImpl.js]
+run-if = os == "win" # Test of Windows backend
+[test_TaskScheduler.js]