diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/taskscheduler | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.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')
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] |