Bug 1676296 Part 1: XPCOM component for Windows Task Scheduler. r=mhowell

Differential Revision: https://phabricator.services.mozilla.com/D97841
This commit is contained in:
Adam Gashlin 2020-12-11 23:17:53 +00:00
Родитель 215e494a20
Коммит 6443deaaee
8 изменённых файлов: 671 добавлений и 0 удалений

Просмотреть файл

@ -110,6 +110,9 @@ DIRS += ["captivedetect"]
if CONFIG["OS_TARGET"] != "Android":
DIRS += ["terminator"]
if CONFIG["MOZ_UPDATE_AGENT"]:
DIRS += ["taskscheduler"]
DIRS += ["build"]
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":

Просмотреть файл

@ -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,
},
]

Просмотреть файл

@ -0,0 +1,23 @@
# -*- 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"]
# 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":
XPIDL_SOURCES += ["nsIWinTaskSchedulerService.idl"]
EXPORTS += ["nsWinTaskScheduler.h"]
SOURCES += ["nsWinTaskScheduler.cpp"]
OS_LIBS += ["taskschd"]
FINAL_LIBRARY = "xul"

Просмотреть файл

@ -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);
};

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -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_ */

Просмотреть файл

@ -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();
});

Просмотреть файл

@ -0,0 +1,2 @@
[test_WinTaskSchedulerService.js]
run-if = os == "win" # Test of Windows only service