diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /toolkit/components/taskscheduler/TaskSchedulerWinImpl.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/taskscheduler/TaskSchedulerWinImpl.sys.mjs')
-rw-r--r-- | toolkit/components/taskscheduler/TaskSchedulerWinImpl.sys.mjs | 316 |
1 files changed, 316 insertions, 0 deletions
diff --git a/toolkit/components/taskscheduler/TaskSchedulerWinImpl.sys.mjs b/toolkit/components/taskscheduler/TaskSchedulerWinImpl.sys.mjs new file mode 100644 index 0000000000..8d9c15c314 --- /dev/null +++ b/toolkit/components/taskscheduler/TaskSchedulerWinImpl.sys.mjs @@ -0,0 +1,316 @@ +/* -*- 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/. */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetters(lazy, { + WinTaskSvc: [ + "@mozilla.org/win-task-scheduler-service;1", + "nsIWinTaskSchedulerService", + ], + XreDirProvider: [ + "@mozilla.org/xre/directory-provider;1", + "nsIXREDirProvider", + ], +}); + +/** + * 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. + */ +export var WinImpl = { + registerTask(id, command, intervalSeconds, options) { + // The folder might not yet exist. + this._createFolderIfNonexistent(); + + const xml = this._formatTaskDefinitionXML( + command, + intervalSeconds, + options + ); + const updateExisting = true; + + lazy.WinTaskSvc.registerTask( + this._taskFolderName(), + this._formatTaskName(id, options), + xml, + updateExisting + ); + }, + + deleteTask(id, options) { + lazy.WinTaskSvc.deleteTask( + this._taskFolderName(), + this._formatTaskName(id, options) + ); + }, + + /** + * Delete all tasks created by this installation. + * + * The Windows Default Browser Agent task is special: it's + * registered by the installer and might run as a different user and + * require permissions to delete. We ignore it and leave it for the + * uninstaller to remove. + */ + deleteAllTasks() { + const taskFolderName = this._taskFolderName(); + + let allTasks; + try { + allTasks = lazy.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)); + + let numberDeleted = 0; + let lastFailedTaskName; + // We need `MOZ_APP_DISPLAYNAME` since that's what the WDBA (written in C++) uses. + const defaultBrowserAgentTaskName = + AppConstants.MOZ_APP_DISPLAYNAME_DO_NOT_USE + + " Default Browser Agent " + + lazy.XreDirProvider.getInstallHash(); + for (const taskName of tasksToDelete) { + if (taskName == defaultBrowserAgentTaskName) { + // Skip the Windows Default Browser Agent task. + continue; + } + + try { + lazy.WinTaskSvc.deleteTask(taskFolderName, taskName); + numberDeleted += 1; + } catch (e) { + lastFailedTaskName = taskName; + } + } + + if (lastFailedTaskName) { + // There's no standard way to chain exceptions, so instead try again, + // which should fail and throw again. It's possible this isn't idempotent + // but we're expecting failures to be due to permission errors, which are + // likely to be static. + lazy.WinTaskSvc.deleteTask(taskFolderName, lastFailedTaskName); + } + + if (allTasks.length == numberDeleted) { + // Deleted every task, remove the folder. + this._deleteFolderIfEmpty(); + } + }, + + taskExists(id, options) { + const taskFolderName = this._taskFolderName(); + + let allTasks; + try { + allTasks = lazy.WinTaskSvc.getFolderTasks(taskFolderName); + } catch (ex) { + if (ex.result == Cr.NS_ERROR_FILE_NOT_FOUND) { + // Folder doesn't exist, so neither do tasks within it. + return false; + } + throw ex; + } + + return allTasks.includes(this._formatTaskName(id, options)); + }, + + _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 settings = doc.querySelector("Settings"); + + 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"; + settings.appendChild(enabled); + } + + if (options?.executionTimeoutSec && options.executionTimeoutSec > 0) { + const timeout = doc.createElementNS(xmlns, "ExecutionTimeLimit"); + timeout.textContent = `PT${options.executionTimeoutSec}S`; + settings.appendChild(timeout); + } + + // Other settings to consider for the future: + // Idle + // Battery + + 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 { + lazy.WinTaskSvc.createFolder(parentName, subName); + } catch (e) { + if (e.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) { + throw e; + } + } + }, + + _deleteFolderIfEmpty() { + const { parentName, subName } = this._taskFolderNameParts(); + + try { + lazy.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, + }; + }, + + /** + * Formats a given task id according to one of two formats. + * + * @param id + * A string representing the identifier of the task to format + * + * @param {Object} options + * Optional, as are all of its properties: + * { + * options.nameVersion + * Specifies whether to search for tasks using nameVersion 1 + * which is `${taskID} ${installHash}` or nameVersion 2 which is + * `${taskID} ${currentUserSid} ${installHash}`. Defaults to nameVersion 2. + * } + * + * @return + * Formatted task name. + */ + _formatTaskName(id, options) { + const installHash = lazy.XreDirProvider.getInstallHash(); + if (options?.nameVersion == 1) { + return `${id} ${installHash}`; + } + const currentUserSid = lazy.WinTaskSvc.getCurrentUserSid(); + return `${id} ${currentUserSid} ${installHash}`; + }, + + _matchAppTaskName(name) { + const installHash = lazy.XreDirProvider.getInstallHash(); + return name.endsWith(` ${installHash}`); + }, + + _updateTaskNameFormat(id) { + const taskFolderName = this._taskFolderName(); + const allTasks = lazy.WinTaskSvc.getFolderTasks(taskFolderName); + const taskNameV1 = this._formatTaskName(id, { nameVersion: 1 }); + const taskNameV2 = this._formatTaskName(id, { nameVersion: 2 }); + if (allTasks.includes(taskNameV1)) { + const taskXML = lazy.WinTaskSvc.getTaskXML(taskFolderName, taskNameV1); + lazy.WinTaskSvc.registerTask(taskFolderName, taskNameV2, taskXML, true); + lazy.WinTaskSvc.deleteTask(taskFolderName, taskNameV1); + } + }, +}; |