Loading toolkit/components/taskscheduler/TaskScheduler.jsm 0 → 100644 +124 −0 Original line number Diff line number Diff line /* -*- 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(); }, }; toolkit/components/taskscheduler/TaskSchedulerWinImpl.jsm 0 → 100644 +229 −0 Original line number Diff line number Diff line /* -*- 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}`); }, }; toolkit/components/taskscheduler/moz.build +3 −0 Original line number Diff line number Diff line Loading @@ -12,9 +12,12 @@ 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"] Loading toolkit/components/taskscheduler/tests/xpcshell/test_TaskScheduler.js 0 → 100644 +40 −0 Original line number Diff line number Diff line /* 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/ ); }); toolkit/components/taskscheduler/tests/xpcshell/test_TaskSchedulerWinImpl.js 0 → 100644 +205 −0 Original line number Diff line number Diff line /* 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 */); }); Loading
toolkit/components/taskscheduler/TaskScheduler.jsm 0 → 100644 +124 −0 Original line number Diff line number Diff line /* -*- 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(); }, };
toolkit/components/taskscheduler/TaskSchedulerWinImpl.jsm 0 → 100644 +229 −0 Original line number Diff line number Diff line /* -*- 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}`); }, };
toolkit/components/taskscheduler/moz.build +3 −0 Original line number Diff line number Diff line Loading @@ -12,9 +12,12 @@ 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"] Loading
toolkit/components/taskscheduler/tests/xpcshell/test_TaskScheduler.js 0 → 100644 +40 −0 Original line number Diff line number Diff line /* 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/ ); });
toolkit/components/taskscheduler/tests/xpcshell/test_TaskSchedulerWinImpl.js 0 → 100644 +205 −0 Original line number Diff line number Diff line /* 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 */); });