Skip to content

Commit

Permalink
Bug 1830071 - Ensure tasks run across all users in Windows r=bytesiz…
Browse files Browse the repository at this point in the history
…ed,application-update-reviewers,nalexander a=pascalc

Differential Revision: https://phabricator.services.mozilla.com/D191226
  • Loading branch information
nipunshukla21 committed Dec 27, 2023
1 parent 0b88eda commit 6aa47a8
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 27 deletions.
63 changes: 53 additions & 10 deletions toolkit/components/taskscheduler/TaskScheduler.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,32 +77,40 @@ export var TaskScheduler = {
* @param intervalSeconds
* Interval at which to run the command, in seconds. Minimum 1800 (30 minutes).
*
* @param options
* @param {Object} options
* Optional, as are all of its properties:
* {
* args
* options.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
* options.workingDirectory
* Working directory for the command. If missing, no working directory is set.
*
* description
* options.description
* A description string that will be visible to system administrators. This should
* be localized. If missing, no description is set.
*
* disabled
* options.disabled
* If true the task will be created disabled, so that it will not be run.
* Ignored on macOS: see comments in TaskSchedulerMacOSImpl.jsm.
* Default false, intended for tests.
*
* executionTimeoutSec
* options.executionTimeoutSec
* Specifies how long (in seconds) the scheduled task can execute for before it is
* automatically stopped by the task scheduler. If a value <= 0 is given, it will be
* ignored.
* This is not currently implemented on macOS.
* On Windows, the default timeout is 72 hours.
*
* options.nameVersion
* Over time, we have needed to change the name format that tasks are registered with.
* When interacting with an up-to-date task, this value can be unspecified and the
* current version of the name format will be used by default. When interacting with
* an out-of-date task using an old naming format, this can be used to specify what
* version of the name should be used. Since the precise naming format is platform
* specific, these version numbers are also platform-specific.
* }
* }
*/
Expand All @@ -123,14 +131,37 @@ export var TaskScheduler = {
/**
* Delete a scheduled task previously created with registerTask.
*
* @param {Object} options
* Optional, as are all of its properties:
* {
* options.nameVersion
* Over time, we have needed to change the name format that tasks are registered with.
* When interacting with an up-to-date task, this value can be unspecified and the
* current version of the name format will be used by default. When interacting with
* an out-of-date task using an old naming format, this can be used to specify what
* version of the name should be used. Since the precise naming format is platform
* specific, these version numbers are also platform-specific.
* }
* @throws NS_ERROR_FILE_NOT_FOUND if the task does not exist.
*/
async deleteTask(id) {
return lazy.gImpl.deleteTask(id);
async deleteTask(id, options) {
return lazy.gImpl.deleteTask(id, options);
},

/**
* Delete all tasks registered by this application.
*
* @param {Object} options
* Optional, as are all of its properties:
* {
* options.nameVersion
* Over time, we have needed to change the name format that tasks are registered with.
* When interacting with an up-to-date task, this value can be unspecified and the
* current version of the name format will be used by default. When interacting with
* an out-of-date task using an old naming format, this can be used to specify what
* version of the name should be used. Since the precise naming format is platform
* specific, these version numbers are also platform-specific.
* }
*/
async deleteAllTasks() {
return lazy.gImpl.deleteAllTasks();
Expand All @@ -142,10 +173,22 @@ export var TaskScheduler = {
* @param id
* A string representing the identifier of the task to look for.
*
* @param {Object} options
* Optional, as are all of its properties:
* {
* options.nameVersion
* Over time, we have needed to change the name format that tasks are registered with.
* When interacting with an up-to-date task, this value can be unspecified and the
* current version of the name format will be used by default. When interacting with
* an out-of-date task using an old naming format, this can be used to specify what
* version of the name should be used. Since the precise naming format is platform
* specific, these version numbers are also platform-specific.
* }
*
* @return
* true if the task exists, otherwise false.
*/
async taskExists(id) {
return lazy.gImpl.taskExists(id);
async taskExists(id, options) {
return lazy.gImpl.taskExists(id, options);
},
};
14 changes: 7 additions & 7 deletions toolkit/components/taskscheduler/TaskSchedulerMacOSImpl.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export var MacOSImpl = {
let uid = await this._uid();
lazy.log.debug(`registerTask: uid=${uid}`);

let label = this._formatLabelForThisApp(id);
let label = this._formatLabelForThisApp(id, options);

// We ignore `options.disabled`, which is test only.
//
Expand Down Expand Up @@ -126,10 +126,10 @@ export var MacOSImpl = {
return true;
},

async deleteTask(id) {
async deleteTask(id, options) {
lazy.log.info(`deleteTask(${id})`);

let label = this._formatLabelForThisApp(id);
let label = this._formatLabelForThisApp(id, options);
return this._deleteTaskByLabel(label);
},

Expand Down Expand Up @@ -207,8 +207,8 @@ export var MacOSImpl = {
lazy.log.debug(`deleteAllTasks: returning ${JSON.stringify(result)}`);
},

async taskExists(id) {
const label = this._formatLabelForThisApp(id);
async taskExists(id, options) {
const label = this._formatLabelForThisApp(id, options);
const path = this._formatPlistPath(label);
return IOUtils.exists(path);
},
Expand Down Expand Up @@ -292,12 +292,12 @@ export var MacOSImpl = {
return serializer.serializeToString(doc);
},

_formatLabelForThisApp(id) {
_formatLabelForThisApp(id, options) {
let installHash = lazy.XreDirProvider.getInstallHash();
return `${AppConstants.MOZ_MACBUNDLE_ID}.${installHash}.${id}`;
},

_labelMatchesThisApp(label) {
_labelMatchesThisApp(label, options) {
let installHash = lazy.XreDirProvider.getInstallHash();
return (
label &&
Expand Down
48 changes: 41 additions & 7 deletions toolkit/components/taskscheduler/TaskSchedulerWinImpl.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@ export var WinImpl = {

lazy.WinTaskSvc.registerTask(
this._taskFolderName(),
this._formatTaskName(id),
this._formatTaskName(id, options),
xml,
updateExisting
);
},

deleteTask(id) {
deleteTask(id, options) {
lazy.WinTaskSvc.deleteTask(
this._taskFolderName(),
this._formatTaskName(id)
this._formatTaskName(id, options)
);
},

Expand Down Expand Up @@ -112,7 +112,7 @@ export var WinImpl = {
}
},

taskExists(id) {
taskExists(id, options) {
const taskFolderName = this._taskFolderName();

let allTasks;
Expand All @@ -126,7 +126,7 @@ export var WinImpl = {
throw ex;
}

return allTasks.includes(this._formatTaskName(id));
return allTasks.includes(this._formatTaskName(id, options));
},

_formatTaskDefinitionXML(command, intervalSeconds, options) {
Expand Down Expand Up @@ -270,13 +270,47 @@ export var WinImpl = {
};
},

_formatTaskName(id) {
/**
* 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();
return `${id} ${installHash}`;
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);
}
},
};
11 changes: 11 additions & 0 deletions toolkit/components/taskscheduler/nsIWinTaskSchedulerService.idl
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ interface nsIWinTaskSchedulerService : nsISupports
*/
AString getTaskXML(in wstring aFolderName, in wstring aTaskName);

/**
* Gets the sid of the current user.
*
* @throws NS_ERROR_NOT_IMPLEMENTED If called on a non-Windows OS.
* @throws NS_ERROR_FAILURE If the user token cannot be found.
* @throws NS_ERROR_ABORT If converting the sid to a string fails.
*
* @returns The sid of the current user.
*/
AString getCurrentUserSid();

/**
* Delete a task.
*
Expand Down
26 changes: 26 additions & 0 deletions toolkit/components/taskscheduler/nsWinTaskScheduler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

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

#include "nsString.h"
Expand Down Expand Up @@ -114,6 +116,30 @@ nsWinTaskSchedulerService::GetTaskXML(const char16_t* aFolderName,
return NS_OK;
}

NS_IMETHODIMP
nsWinTaskSchedulerService::GetCurrentUserSid(nsAString& aUserSid) {
#ifndef XP_WIN
return NS_ERROR_NOT_IMPLEMENTED;
#else // !XP_WIN
DWORD tokenLen;
LPWSTR stringSid;
BYTE tokenBuf[TOKEN_USER_MAX_SIZE];
PTOKEN_USER tokenInfo = reinterpret_cast<PTOKEN_USER>(tokenBuf);
BOOL success = GetTokenInformation(GetCurrentProcessToken(), TokenUser,
tokenInfo, sizeof(tokenBuf), &tokenLen);
if (!success) {
return NS_ERROR_FAILURE;
}
success = ConvertSidToStringSidW(tokenInfo->User.Sid, &stringSid);
if (!success) {
return NS_ERROR_ABORT;
}
aUserSid.Assign(stringSid);
LocalFree(stringSid);
return NS_OK;
#endif
}

NS_IMETHODIMP
nsWinTaskSchedulerService::RegisterTask(const char16_t* aFolderName,
const char16_t* aTaskName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,92 @@ add_task(async function test_create() {
);
Assert.equal(WinSvc.validateTaskDefinition(basicXML), 0 /* S_OK */);
});

add_task(async function test_migrate() {
// Create task name with nameVersion1
const taskName = "test-task-1";
const rawTaskNameV1 = WinImpl._formatTaskName(taskName, { nameVersion: 1 });
const rawTaskNameV2 = WinImpl._formatTaskName(taskName, { nameVersion: 2 });
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 expectedIntervalOut = "PT2H"; // 2 hours

const queries = [
["Actions Exec Command", exePath],
["Actions Exec WorkingDirectory", workingDir],
["Actions Exec Arguments", expectedArgsOutStr],
["RegistrationInfo Description", description],
["RegistrationInfo Author", Services.appinfo.vendor],
["Settings Enabled", "false"],
["Triggers TimeTrigger Repetition Interval", expectedIntervalOut],
];

await TaskScheduler.registerTask(taskName, exePath, intervalSecsIn, {
disabled: true,
args: argsIn,
description,
workingDirectory: workingDir,
nameVersion: 1,
});

ok(
WinImpl.taskExists(taskName, { nameVersion: 1 }),
"Task exists with nameVersion1"
);
const originalTaskXML = WinSvc.getTaskXML(folderName, rawTaskNameV1);
const parser = new DOMParser();
const docV1 = parser.parseFromString(originalTaskXML, "text/xml");

Assert.equal(docV1.documentElement.tagName, "Task");

// Check for the values set above
for (let [sel, expected] of queries) {
Assert.equal(
docV1.querySelector(sel).textContent,
expected,
`Task V1 ${sel} had expected textContent`
);
}

// Update task name format to nameVersion2
WinImpl._updateTaskNameFormat(taskName);
ok(
WinImpl.taskExists(taskName, { nameVersion: 2 }),
"Task exists with nameVersion2"
);
ok(
!WinImpl.taskExists(taskName, { nameVersion: 1 }),
"Task with nameVersion1 successfully deleted"
);

// Check that the new task XML is still valid
const newTaskXML = WinSvc.getTaskXML(folderName, rawTaskNameV2);
Assert.equal(WinSvc.validateTaskDefinition(newTaskXML), 0 /* S_OK */);
const docV2 = parser.parseFromString(newTaskXML, "text/xml");

Assert.equal(docV2.documentElement.tagName, "Task");

// Check that the updated values still match the provided ones.
for (let [sel, expected] of queries) {
Assert.equal(
docV2.querySelector(sel).textContent,
expected,
`Task V2 ${sel} had expected textContent`
);
}
});
Loading

0 comments on commit 6aa47a8

Please sign in to comment.