Bug 1515451 Part 1 - Update agent scheduled task r=mhowell,bytesized,nalexander

This is the skeleton for interacting with the Windows Task Scheduler, it
produces an exe that can register and unregister itself as a scheduled
task.

The schedule is to run once daily. Bug 1568287 is reserved for discussions
of other trigger patterns, possibly depending on the channel.

This uses a the Windows Event Log for logging, Bug 1343676 deals with
possibly extending that to a rotating log file.

Differential Revision: https://phabricator.services.mozilla.com/D35507

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Adam Gashlin 2020-03-11 18:04:11 +00:00
Родитель b154619cbb
Коммит 4b591f8eae
8 изменённых файлов: 1203 добавлений и 0 удалений

11
Cargo.lock сгенерированный
Просмотреть файл

@ -4661,6 +4661,17 @@ dependencies = [
"void",
]
[[package]]
name = "updateagent"
version = "0.1.0"
dependencies = [
"chrono",
"comedy",
"failure",
"log",
"winapi 0.3.7",
]
[[package]]
name = "url"
version = "2.1.0"

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

@ -14,6 +14,7 @@ members = [
"security/manager/ssl/osclientcerts",
"testing/geckodriver",
"toolkit/crashreporter/rust",
"toolkit/components/updateagent",
"toolkit/library/gtest/rust",
"toolkit/library/rust/",
]

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

@ -0,0 +1,21 @@
[package]
name = "updateagent"
version = "0.1.0"
authors = ["The Mozilla Project Developers"]
license = "MPL-2.0"
autobins = false
edition = "2018"
[target."cfg(windows)".dependencies]
chrono = "0.4"
comedy = "0.1"
failure = "0.1"
log = "0.4"
[target."cfg(windows)".dependencies.winapi]
version = "0.3.7"
features = ["minwindef", "ntdef", "oaidl", "oleauto", "taskschd", "winbase", "winerror", "winnt", "wtypes"]
[[bin]]
name = "updateagent"
path = "src/main.rs"

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

@ -0,0 +1,65 @@
/* 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 https://mozilla.org/MPL/2.0/. */
//! Very simple implementation of logging via the Windows Event Log
use std::ptr;
use crate::ole_utils::to_u16_nul;
use log::{Level, Metadata, Record};
use winapi::shared::minwindef::WORD;
use winapi::um::{winbase, winnt};
pub struct EventLogger;
impl log::Log for EventLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= log::max_level()
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let name = to_u16_nul(crate::DESCRIPTION);
let msg = to_u16_nul(format!("{} - {}", record.level(), record.args()));
// Open and close the event log handle on every message, for simplicity.
let event_log;
unsafe {
event_log = winbase::RegisterEventSourceW(ptr::null(), name.as_ptr());
if event_log.is_null() {
return;
}
}
let level = match record.level() {
Level::Error => winnt::EVENTLOG_ERROR_TYPE,
Level::Warn => winnt::EVENTLOG_WARNING_TYPE,
Level::Info | Level::Debug | Level::Trace => winnt::EVENTLOG_INFORMATION_TYPE,
};
unsafe {
// mut only to match the LPCWSTR* signature
let mut msg_array: [*const u16; 1] = [msg.as_ptr()];
let _ = winbase::ReportEventW(
event_log,
level,
0, // no category
0, // event id 0
ptr::null_mut(), // no user sid
msg_array.len() as WORD, // string count
0, // 0 bytes raw data
msg_array.as_mut_ptr(), // strings
ptr::null_mut(), // no raw data
);
let _ = winbase::DeregisterEventSource(event_log);
}
}
fn flush(&self) {}
}

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

@ -0,0 +1,161 @@
/* 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 https://mozilla.org/MPL/2.0/. */
// This code is Windows-specific, don't build this module for another platform.
#![cfg(windows)]
// We want to use the "windows" subsystem to avoid popping up a console window. This also
// prevents Windows consoles from picking up output, however, so it is disabled when debugging.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod event_log;
mod ole_utils;
mod task_setup;
pub mod taskschd;
use std::env;
use std::ffi::OsString;
use std::process;
use comedy::com::ComApartmentScope;
use log::{debug, error, info};
// Used as the name of the task folder and author of the task
pub static VENDOR: &str = "Mozilla";
// Used as the description of the task and the event log application
pub static DESCRIPTION: &str = "Mozilla Update Agent";
fn main() {
log::set_logger(&event_log::EventLogger).unwrap();
log::set_max_level(log::LevelFilter::Info);
// TODO: Appropriate threading and security settings will depend on the main work the task
// will be doing.
let _com = ComApartmentScope::init_mta().unwrap();
process::exit(match fallible_main() {
Ok(_) => {
debug!("success");
0
}
Err(e) => {
error!("{}", e);
1
}
});
}
/// Command types
pub mod cmd {
// Create a new task, removing any old one.
// `updateagent.exe register-task TaskName [TaskArgs ...]`
pub static REGISTER_TASK: &str = "register-task";
pub static REGISTER_TASK_LOCAL_SERVICE: &str = "register-task-local-service";
// Create a new task, removing any old one, but copying its schedule as appropriate.
pub static UPDATE_TASK: &str = "update-task";
pub static UPDATE_TASK_LOCAL_SERVICE: &str = "update-task-local-service";
// Remove the task.
pub static UNREGISTER_TASK: &str = "unregister-task";
// Request to be run immediately by Task Scheduler.
pub static RUN_ON_DEMAND: &str = "run-on-demand";
// The task is set up to execute this command, using the TaskArgs from registration.
// `updateagent.exe do-task [TaskArgs ...]`
pub static DO_TASK: &str = "do-task";
#[derive(Clone)]
pub enum Command {
RegisterTask,
RegisterTaskLocalService,
UpdateTask,
UpdateTaskLocalService,
UnregisterTask,
RunOnDemand,
DoTask,
}
impl Command {
pub fn parse(s: &str) -> Option<Command> {
use Command::*;
// Build a map to lookup the string. This only runs once so performance isn't critical.
let lookup_map: std::collections::HashMap<_, _> = [
(REGISTER_TASK, RegisterTask),
(REGISTER_TASK_LOCAL_SERVICE, RegisterTaskLocalService),
(UPDATE_TASK, UpdateTask),
(UPDATE_TASK_LOCAL_SERVICE, UpdateTaskLocalService),
(UNREGISTER_TASK, UnregisterTask),
(RUN_ON_DEMAND, RunOnDemand),
(DO_TASK, DoTask),
]
.iter()
.cloned()
.collect();
lookup_map.get(s).cloned()
}
}
}
use cmd::Command;
pub fn fallible_main() -> Result<(), String> {
let args_os: Vec<_> = env::args_os().collect();
let command = {
let command_str = args_os
.get(1)
.ok_or_else(|| String::from("missing command"))?
.to_string_lossy()
.to_owned();
Command::parse(&command_str)
.ok_or_else(|| format!("unknown command \"{}\"", command_str))?
};
// all commands except DoTask take TaskName as args_os[2]
let maybe_task_name = args_os
.get(2)
.ok_or_else(|| String::from("missing TaskName"));
match command {
Command::RegisterTask
| Command::RegisterTaskLocalService
| Command::UpdateTask
| Command::UpdateTaskLocalService => {
let exe = env::current_exe().map_err(|e| format!("get current exe failed: {}", e))?;
let task_name = maybe_task_name?;
task_setup::register(&*exe, task_name, &args_os[3..], command)
.map_err(|e| format!("register failed: {}", e))
}
Command::UnregisterTask => {
if args_os.len() != 3 {
return Err("unregister-task takes only one argument: TaskName".into());
}
task_setup::unregister(maybe_task_name?)
.map_err(|e| format!("unregister failed: {}", e))
}
Command::RunOnDemand => {
if args_os.len() != 3 {
return Err("run-on-demand takes only one argument: TaskName".into());
}
task_setup::run_on_demand(maybe_task_name?)
.map_err(|e| format!("run on demand failed: {}", e))
}
Command::DoTask => task_action(&args_os[2..]),
}
}
fn task_action(args: &[OsString]) -> Result<(), String> {
// TODO actual task content
info!("task_action({:?})", args);
Ok(())
}

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

@ -0,0 +1,151 @@
/* 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 https://mozilla.org/MPL/2.0/. */
use std::convert::TryInto;
use std::ffi::OsStr;
use std::mem;
use std::os::windows::ffi::OsStrExt;
use std::ptr::NonNull;
use std::slice;
use winapi::shared::{winerror, wtypes};
use winapi::um::{oaidl, oleauto};
use comedy::HResult;
/// Conveniently create and destroy a `BSTR`.
///
/// This is called `BString` by analogy to `String` since it owns the data.
///
/// The internal `BSTR` is always non-null, even for an empty string, for simple safety reasons.
#[derive(Debug)]
pub struct BString(NonNull<u16>);
impl BString {
pub fn from_slice(v: impl AsRef<[u16]>) -> Result<BString, HResult> {
let v = v.as_ref();
let real_len = v.len();
let len = real_len
.try_into()
.map_err(|_| HResult::new(winerror::E_OUTOFMEMORY))?;
let bs = unsafe { oleauto::SysAllocStringLen(v.as_ptr(), len) };
Ok(Self(NonNull::new(bs).ok_or_else(|| {
HResult::new(winerror::E_OUTOFMEMORY).function("SysAllocStringLen")
})?))
}
pub fn from_os_str(s: impl AsRef<OsStr>) -> Result<BString, HResult> {
BString::from_slice(s.as_ref().encode_wide().collect::<Vec<_>>().as_slice())
}
/// Take ownership of a `BSTR`.
///
/// This will be freed when the `BString` is dropped, so the pointer shouldn't be used
/// after calling this function.
///
/// Returns `None` if the pointer is null; though this means an empty string in most
/// contexts where `BSTR` is used, `BString` is always non-null.
pub unsafe fn from_raw(p: *mut u16) -> Option<Self> {
Some(Self(NonNull::new(p)?))
}
/// Get a pointer to the `BSTR`.
///
/// The caller must ensure that the `BString` outlives the pointer this function returns,
/// or else it will end up pointing to garbage.
///
/// This pointer shouldn't be written to, but most APIs require a mutable pointer.
pub fn as_raw_ptr(&self) -> *mut u16 {
self.0.as_ptr()
}
/// Build a raw `VARIANT`, essentially a typed pointer.
///
/// The caller must ensure that the `BString` outlives the `VARIANT` this function returns,
/// or else it will end up pointing to garbage.
///
/// This is meant for passing by value to Windows APIs.
pub fn as_raw_variant(&self) -> oaidl::VARIANT {
unsafe {
let mut v: oaidl::VARIANT = mem::zeroed();
{
let tv = v.n1.n2_mut();
*tv.n3.bstrVal_mut() = self.as_raw_ptr();
tv.vt = wtypes::VT_BSTR as wtypes::VARTYPE;
}
v
}
}
}
impl Drop for BString {
fn drop(&mut self) {
unsafe { oleauto::SysFreeString(self.0.as_ptr()) }
}
}
impl AsRef<[u16]> for BString {
fn as_ref(&self) -> &[u16] {
unsafe {
let len = oleauto::SysStringLen(self.0.as_ptr());
slice::from_raw_parts(self.0.as_ptr(), len as usize)
}
}
}
/// Try to convert, decorate `Err` with call site info
#[macro_export]
macro_rules! try_to_bstring {
($ex:expr) => {
$crate::ole_utils::BString::from_os_str($ex).map_err(|e| e.file_line(file!(), line!()))
};
}
pub fn empty_variant() -> oaidl::VARIANT {
unsafe {
let mut v: oaidl::VARIANT = mem::zeroed();
{
let tv = v.n1.n2_mut();
tv.vt = wtypes::VT_EMPTY as wtypes::VARTYPE;
}
v
}
}
pub trait OptionBstringExt {
fn as_raw_variant(&self) -> oaidl::VARIANT;
}
/// Shorthand for unwrapping, returns `BString::as_raw_variant()` or `empty_variant()`
impl OptionBstringExt for Option<&BString> {
fn as_raw_variant(&self) -> oaidl::VARIANT {
self.map(|bs| bs.as_raw_variant())
.unwrap_or_else(empty_variant)
}
}
// Note: A `VARIANT_BOOL` is not a `VARIANT`, rather it would go into a `VARIANT` of type
// `VT_BOOL`. Some APIs use it directly.
pub trait IntoVariantBool {
fn into_variant_bool(self) -> wtypes::VARIANT_BOOL;
}
impl IntoVariantBool for bool {
fn into_variant_bool(self) -> wtypes::VARIANT_BOOL {
if self {
wtypes::VARIANT_TRUE
} else {
wtypes::VARIANT_FALSE
}
}
}
pub fn to_u16_nul(s: impl AsRef<OsStr>) -> Vec<u16> {
s.as_ref().encode_wide().chain(Some(0)).collect()
}

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

@ -0,0 +1,209 @@
/* 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 https://mozilla.org/MPL/2.0/. */
use crate::cmd::Command;
use std::ffi::{OsStr, OsString};
use std::path::Path;
use comedy::HResult;
use log::warn;
use crate::cmd;
use crate::ole_utils::BString;
use crate::taskschd::{hr_is_not_found, TaskService};
use crate::try_to_bstring;
fn folder_name() -> Result<BString, HResult> {
try_to_bstring!(crate::VENDOR)
}
pub fn register(
exe: &Path,
name: &OsStr,
args: &[OsString],
command: Command,
) -> Result<(), failure::Error> {
let name = try_to_bstring!(name)?;
let folder_name = folder_name()?;
let local_service = match command {
Command::RegisterTask | Command::UpdateTask => false,
Command::RegisterTaskLocalService | Command::UpdateTaskLocalService => true,
_ => unreachable!(),
};
let update_task = match command {
Command::RegisterTask | Command::RegisterTaskLocalService => false,
Command::UpdateTask | Command::UpdateTaskLocalService => true,
_ => unreachable!(),
};
let mut service = TaskService::connect_local()?;
// Get or create the folder
let mut folder = service.get_folder(&folder_name).or_else(|e| {
if hr_is_not_found(&e) {
service
.get_root_folder()
.and_then(|mut root| root.create_folder(&folder_name))
} else {
Err(e)
}
})?;
// When updating, we still delete and recreate the task.
// The only part that is currently copied over is the start boundary of the
// daily trigger, since that it otherwise set to 5 minutes before the registration; in this way
// updates won't delay the task's next scheduled run time.
// TODO: We may want to also track if the task was disabled.
let start_time = if update_task {
// Ignoring any failures, if we can't get the time for any reason we choose a new one.
folder
.get_task(&name)
.ok()
.and_then(|mut task| task.get_definition().ok())
.and_then(|mut def| def.get_daily_triggers().ok())
.and_then(|mut triggers| {
// Currently we are only using 1 daily trigger.
triggers
.get_mut(0)
.and_then(|trigger| trigger.get_StartBoundary().ok())
})
} else {
None
};
folder.delete_task(&name).unwrap_or_else(|e| {
// Don't even warn if the task didn't exist.
if !hr_is_not_found(&e) {
warn!("delete task failed: {}", e);
}
});
let mut task_def = service.new_task_definition()?;
{
let mut task_args = vec![OsString::from(cmd::DO_TASK)];
task_args.extend_from_slice(args);
let mut action = task_def.add_exec_action()?;
action.put_Path(exe)?;
action.put_Arguments(task_args.as_slice())?;
// TODO working directory?
}
{
let mut settings = task_def.get_settings()?;
settings.put_DisallowStartIfOnBatteries(false)?;
settings.put_StopIfGoingOnBatteries(false)?;
settings.put_StartWhenAvailable(true)?;
settings.put_ExecutionTimeLimit(chrono::Duration::minutes(5))?;
}
{
let mut info = task_def.get_registration_info()?;
info.put_Author(&try_to_bstring!(crate::VENDOR)?)?;
info.put_Description(&try_to_bstring!(crate::DESCRIPTION)?)?;
}
// A daily trigger starting 5 minutes ago.
{
let mut daily_trigger = task_def.add_daily_trigger()?;
if let Some(ref start_time) = start_time {
daily_trigger.put_StartBoundary_BString(start_time)?;
} else {
daily_trigger.put_StartBoundary(chrono::Utc::now() - chrono::Duration::minutes(5))?;
}
daily_trigger.put_DaysInterval(1)?;
// TODO: 12-hourly trigger? logon trigger?
}
let service_account = if local_service {
Some(try_to_bstring!("NT AUTHORITY\\LocalService")?)
} else {
None
};
let mut registered_task = task_def.create(&mut folder, &name, service_account.as_ref())?;
if local_service {
// SDDL seem to be the only way to set the security descriptor.
// https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-string-format
// Setting just the DACL here allows us to avoid specifying the ownership information,
// which I think is required if SDDL is provided on initial task registration.
let sddl = try_to_bstring!(concat!(
"D:(", // DACL
"A;", // ace_type = Allow
";", // ace_flags = none
"GRGX;", // rights = Generic Read, Generic Execute
";;", // object_guid, inherit_object_guid = none
"BU)" // account_sid = Built-in users
))?;
registered_task.set_sd(&sddl)?;
}
Ok(())
}
pub fn unregister(name: &OsStr) -> Result<(), failure::Error> {
let name = try_to_bstring!(name)?;
let folder_name = folder_name()?;
let mut service = TaskService::connect_local()?;
let maybe_folder = service.get_folder(&folder_name);
let mut folder = match maybe_folder {
Err(e) => {
if hr_is_not_found(&e) {
// Just warn and exit if the folder didn't exist.
warn!("failed to unregister: task folder didn't exist");
return Ok(());
} else {
// Other errors are fatal.
return Err(e.into());
}
}
Ok(folder) => folder,
};
folder.delete_task(&name).or_else(|e| {
if hr_is_not_found(&e) {
// Only warn if the task didn't exist, still try to remove the folder below.
warn!("failed to unregister task that didn't exist");
Ok(())
} else {
// Other errors are fatal.
Err(e)
}
})?;
let count = folder.get_task_count(true).unwrap_or_else(|e| {
warn!("failed getting task count: {}", e);
1
});
if count == 0 {
let result = service
.get_root_folder()
.and_then(|mut root| root.delete_folder(&folder_name));
if let Err(e) = result {
warn!("failed deleting folder: {}", e);
}
}
Ok(())
}
pub fn run_on_demand(name: &OsStr) -> Result<(), failure::Error> {
let name = try_to_bstring!(name)?;
let folder_name = folder_name()?;
let mut service = TaskService::connect_local()?;
let task = service.get_folder(&folder_name)?.get_task(&name)?;
task.run()?;
Ok(())
}

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

@ -0,0 +1,584 @@
/* 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 https://mozilla.org/MPL/2.0/. */
//! A partial type-safe interface for Windows Task Scheduler 2.0
//!
//! This provides structs thinly wrapping the taskschd interfaces, with methods implemented as
//! they've been needed for the update agent.
//!
//! If it turns out that much more flexibility is needed in task definitions, it may be worth
//! generating an XML string and using `ITaskFolder::RegisterTask` or
//! `ITaskDefinition::put_XmlText`, rather than adding more and more boilerplate here.
//!
//! See https://docs.microsoft.com/windows/win32/taskschd/task-scheduler-start-page for
//! Microsoft's documentation.
use std::ffi::{OsStr, OsString};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::path::Path;
use std::ptr;
use comedy::com::{create_instance_inproc_server, ComRef};
use comedy::error::{HResult, Win32Error};
use comedy::{com_call, com_call_getter};
use failure::Fail;
use crate::ole_utils::{empty_variant, BString, IntoVariantBool, OptionBstringExt};
use crate::try_to_bstring;
use winapi::shared::{
ntdef::{LONG, SHORT},
winerror::{
ERROR_ALREADY_EXISTS, ERROR_BAD_ARGUMENTS, ERROR_FILE_NOT_FOUND, E_ACCESSDENIED,
SCHED_E_SERVICE_NOT_RUNNING,
},
};
use winapi::um::taskschd::{
self, IDailyTrigger, IExecAction, IRegisteredTask, IRegistrationInfo, IRunningTask,
ITaskDefinition, ITaskFolder, ITaskService, ITaskSettings, ITrigger, ITriggerCollection,
};
/// Check if the `HResult` represents the win32 `ERROR_FILE_NOT_FOUND`, returned by
/// several task scheduler methods.
pub fn hr_is_not_found(hr: &HResult) -> bool {
hr.code() == HResult::from(Win32Error::new(ERROR_FILE_NOT_FOUND)).code()
}
/// Check if the `HResult` represents the win32 `ERROR_ALREADY_EXISTS`, returned by
/// several task scheduler methods.
pub fn hr_is_already_exists(hr: &HResult) -> bool {
hr.code() == HResult::from(Win32Error::new(ERROR_ALREADY_EXISTS)).code()
}
// These macros simplify wrapping `put_*` property methods. They are each slightly different;
// I found it significantly more confusing to try to combine them.
/// put a bool, converting to `VARIANT_BOOL`
macro_rules! bool_putter {
($interface:ident :: $method:ident) => {
#[allow(non_snake_case)]
pub fn $method(&mut self, v: bool) -> Result<(), HResult> {
let v = v.into_variant_bool();
unsafe {
com_call!(
self.0,
$interface :: $method(v))?;
}
Ok(())
}
}
}
/// put a value that is already available as a `BString`
macro_rules! bstring_putter {
($interface:ident :: $method:ident) => {
#[allow(non_snake_case)]
pub fn $method(&mut self, v: &BString) -> Result<(), HResult> {
unsafe {
com_call!(
self.0,
$interface :: $method(v.as_raw_ptr()))?;
}
Ok(())
}
}
}
/// put a `chrono::DateTime` value
macro_rules! datetime_putter {
($interface:ident :: $method:ident) => {
#[allow(non_snake_case)]
pub fn $method(&mut self, v: chrono::DateTime<chrono::Utc>) -> Result<(), HResult> {
let v = try_to_bstring!(v.to_rfc3339_opts(chrono::SecondsFormat::Secs, true))?;
unsafe {
com_call!(
self.0,
$interface :: $method(v.as_raw_ptr()))?;
}
Ok(())
}
}
}
/// put a value of type `$ty`, which implements `AsRef<OsStr>`
macro_rules! to_os_str_putter {
($interface:ident :: $method:ident, $ty:ty) => {
#[allow(non_snake_case)]
pub fn $method(&mut self, v: $ty) -> Result<(), HResult> {
let v = try_to_bstring!(v)?;
unsafe {
com_call!(
self.0,
$interface :: $method(v.as_raw_ptr()))?;
}
Ok(())
}
}
}
/// put a value of type `$ty`, which implements `ToString`
macro_rules! to_string_putter {
($interface:ident :: $method:ident, $ty:ty) => {
#[allow(non_snake_case)]
pub fn $method(&mut self, v: $ty) -> Result<(), HResult> {
let v = try_to_bstring!(v.to_string())?;
unsafe {
com_call!(
self.0,
$interface :: $method(v.as_raw_ptr()))?;
}
Ok(())
}
}
}
pub struct TaskService(ComRef<ITaskService>);
impl TaskService {
pub fn connect_local() -> Result<TaskService, ConnectTaskServiceError> {
use self::ConnectTaskServiceError::*;
let task_service = create_instance_inproc_server::<taskschd::TaskScheduler, ITaskService>()
.map_err(CreateInstanceFailed)?;
// Connect to local service with no credentials.
unsafe {
com_call!(
task_service,
ITaskService::Connect(
empty_variant(),
empty_variant(),
empty_variant(),
empty_variant()
)
)
}
.map_err(|hr| match hr.code() {
E_ACCESSDENIED => AccessDenied(hr),
SCHED_E_SERVICE_NOT_RUNNING => ServiceNotRunning(hr),
_ => ConnectFailed(hr),
})?;
Ok(TaskService(task_service))
}
pub fn get_root_folder(&mut self) -> Result<TaskFolder, HResult> {
self.get_folder(&try_to_bstring!("\\")?)
}
pub fn get_folder(&mut self, path: &BString) -> Result<TaskFolder, HResult> {
unsafe {
com_call_getter!(
|folder| self.0,
ITaskService::GetFolder(path.as_raw_ptr(), folder)
)
}
.map(TaskFolder)
}
pub fn new_task_definition(&mut self) -> Result<TaskDefinition, HResult> {
unsafe {
com_call_getter!(
|task_def| self.0,
ITaskService::NewTask(
0, // flags (reserved)
task_def,
)
)
}
.map(TaskDefinition)
}
}
#[derive(Clone, Debug, Fail)]
pub enum ConnectTaskServiceError {
#[fail(display = "{}", _0)]
CreateInstanceFailed(#[fail(cause)] HResult),
#[fail(display = "Access is denied to connect to the Task Scheduler service")]
AccessDenied(#[fail(cause)] HResult),
#[fail(display = "The Task Scheduler service is not running")]
ServiceNotRunning(#[fail(cause)] HResult),
#[fail(display = "{}", _0)]
ConnectFailed(#[fail(cause)] HResult),
}
pub struct TaskFolder(ComRef<ITaskFolder>);
impl TaskFolder {
pub fn get_task(&mut self, task_name: &BString) -> Result<RegisteredTask, HResult> {
unsafe {
com_call_getter!(
|task| self.0,
ITaskFolder::GetTask(task_name.as_raw_ptr(), task)
)
}
.map(RegisteredTask)
}
pub fn get_task_count(&mut self, include_hidden: bool) -> Result<LONG, HResult> {
use self::taskschd::IRegisteredTaskCollection;
let flags = if include_hidden {
taskschd::TASK_ENUM_HIDDEN
} else {
0
};
unsafe {
let tasks = com_call_getter!(|t| self.0, ITaskFolder::GetTasks(flags as LONG, t))?;
let mut count = 0;
com_call!(tasks, IRegisteredTaskCollection::get_Count(&mut count))?;
Ok(count)
}
}
pub fn create_folder(&mut self, path: &BString) -> Result<TaskFolder, HResult> {
let sddl = empty_variant();
unsafe {
com_call_getter!(
|folder| self.0,
ITaskFolder::CreateFolder(path.as_raw_ptr(), sddl, folder)
)
}
.map(TaskFolder)
}
pub fn delete_folder(&mut self, path: &BString) -> Result<(), HResult> {
unsafe {
com_call!(
self.0,
ITaskFolder::DeleteFolder(
path.as_raw_ptr(),
0, // flags (reserved)
)
)?;
}
Ok(())
}
pub fn delete_task(&mut self, task_name: &BString) -> Result<(), HResult> {
unsafe {
com_call!(
self.0,
ITaskFolder::DeleteTask(
task_name.as_raw_ptr(),
0, // flags (reserved)
)
)?;
}
Ok(())
}
}
pub struct TaskDefinition(ComRef<ITaskDefinition>);
impl TaskDefinition {
pub fn get_settings(&mut self) -> Result<TaskSettings, HResult> {
unsafe { com_call_getter!(|s| self.0, ITaskDefinition::get_Settings(s)) }.map(TaskSettings)
}
pub fn get_registration_info(&mut self) -> Result<RegistrationInfo, HResult> {
unsafe { com_call_getter!(|ri| self.0, ITaskDefinition::get_RegistrationInfo(ri)) }
.map(RegistrationInfo)
}
unsafe fn add_action<T: winapi::Interface>(
&mut self,
action_type: taskschd::TASK_ACTION_TYPE,
) -> Result<ComRef<T>, HResult> {
use self::taskschd::IActionCollection;
let actions = com_call_getter!(|ac| self.0, ITaskDefinition::get_Actions(ac))?;
let action = com_call_getter!(|a| actions, IActionCollection::Create(action_type, a))?;
action.cast()
}
pub fn add_exec_action(&mut self) -> Result<ExecAction, HResult> {
unsafe { self.add_action(taskschd::TASK_ACTION_EXEC) }.map(ExecAction)
}
unsafe fn add_trigger<T: winapi::Interface>(
&mut self,
trigger_type: taskschd::TASK_TRIGGER_TYPE2,
) -> Result<ComRef<T>, HResult> {
let triggers = com_call_getter!(|tc| self.0, ITaskDefinition::get_Triggers(tc))?;
let trigger = com_call_getter!(|t| triggers, ITriggerCollection::Create(trigger_type, t))?;
trigger.cast()
}
pub fn add_daily_trigger(&mut self) -> Result<DailyTrigger, HResult> {
unsafe { self.add_trigger(taskschd::TASK_TRIGGER_DAILY) }.map(DailyTrigger)
}
pub fn get_daily_triggers(&mut self) -> Result<Vec<DailyTrigger>, HResult> {
let mut found_triggers = Vec::new();
unsafe {
let triggers = com_call_getter!(|tc| self.0, ITaskDefinition::get_Triggers(tc))?;
let mut count = 0;
com_call!(triggers, ITriggerCollection::get_Count(&mut count))?;
// Item indexes start at 1
for i in 1..=count {
let trigger = com_call_getter!(|t| triggers, ITriggerCollection::get_Item(i, t))?;
let mut trigger_type = 0;
com_call!(trigger, ITrigger::get_Type(&mut trigger_type))?;
if trigger_type == taskschd::TASK_TRIGGER_DAILY {
found_triggers.push(DailyTrigger(trigger.cast()?))
}
}
}
Ok(found_triggers)
}
pub fn create(
&mut self,
folder: &mut TaskFolder,
task_name: &BString,
service_account: Option<&BString>,
) -> Result<RegisteredTask, HResult> {
self.register_impl(folder, task_name, service_account, taskschd::TASK_CREATE)
}
fn register_impl(
&mut self,
folder: &mut TaskFolder,
task_name: &BString,
service_account: Option<&BString>,
creation_flags: taskschd::TASK_CREATION,
) -> Result<RegisteredTask, HResult> {
let task_definition = self.0.as_raw_ptr();
let password = empty_variant();
let logon_type = if service_account.is_some() {
taskschd::TASK_LOGON_SERVICE_ACCOUNT
} else {
taskschd::TASK_LOGON_INTERACTIVE_TOKEN
};
let sddl = empty_variant();
let registered_task = unsafe {
com_call_getter!(
|rt| folder.0,
ITaskFolder::RegisterTaskDefinition(
task_name.as_raw_ptr(),
task_definition,
creation_flags as LONG,
service_account.as_raw_variant(),
password,
logon_type,
sddl,
rt,
)
)?
};
Ok(RegisteredTask(registered_task))
}
pub fn get_xml(task_definition: &ComRef<ITaskDefinition>) -> Result<OsString, String> {
unsafe {
let mut xml = ptr::null_mut();
com_call!(task_definition, ITaskDefinition::get_XmlText(&mut xml))
.map_err(|e| format!("{}", e))?;
Ok(OsString::from_wide(
BString::from_raw(xml)
.ok_or_else(|| "null xml".to_string())?
.as_ref(),
))
}
}
}
pub struct TaskSettings(ComRef<ITaskSettings>);
impl TaskSettings {
bool_putter!(ITaskSettings::put_AllowDemandStart);
bool_putter!(ITaskSettings::put_DisallowStartIfOnBatteries);
to_string_putter!(ITaskSettings::put_ExecutionTimeLimit, chrono::Duration);
bool_putter!(ITaskSettings::put_Hidden);
#[allow(non_snake_case)]
pub fn put_MultipleInstances(&mut self, v: InstancesPolicy) -> Result<(), HResult> {
unsafe {
com_call!(self.0, ITaskSettings::put_MultipleInstances(v as u32))?;
}
Ok(())
}
bool_putter!(ITaskSettings::put_RunOnlyIfIdle);
bool_putter!(ITaskSettings::put_RunOnlyIfNetworkAvailable);
bool_putter!(ITaskSettings::put_StartWhenAvailable);
bool_putter!(ITaskSettings::put_StopIfGoingOnBatteries);
bool_putter!(ITaskSettings::put_Enabled);
bool_putter!(ITaskSettings::put_WakeToRun);
}
pub struct RegistrationInfo(ComRef<IRegistrationInfo>);
impl RegistrationInfo {
bstring_putter!(IRegistrationInfo::put_Author);
bstring_putter!(IRegistrationInfo::put_Description);
}
#[derive(Clone, Copy, Debug)]
#[repr(u32)]
pub enum InstancesPolicy {
Parallel = taskschd::TASK_INSTANCES_PARALLEL,
Queue = taskschd::TASK_INSTANCES_QUEUE,
IgnoreNew = taskschd::TASK_INSTANCES_IGNORE_NEW,
StopExisting = taskschd::TASK_INSTANCES_STOP_EXISTING,
}
pub struct DailyTrigger(ComRef<IDailyTrigger>);
impl DailyTrigger {
datetime_putter!(IDailyTrigger::put_StartBoundary);
// I'd like to have this only use the type-safe DateTime, but when copying it seems less
// error-prone to use the string directly rather than try to parse it and then convert it back
// to string.
#[allow(non_snake_case)]
pub fn put_StartBoundary_BString(&mut self, v: &BString) -> Result<(), HResult> {
unsafe {
com_call!(self.0, IDailyTrigger::put_StartBoundary(v.as_raw_ptr()))?;
}
Ok(())
}
#[allow(non_snake_case)]
pub fn get_StartBoundary(&mut self) -> Result<BString, HResult> {
unsafe {
let mut start_boundary = ptr::null_mut();
let hr = com_call!(
self.0,
IDailyTrigger::get_StartBoundary(&mut start_boundary)
)?;
BString::from_raw(start_boundary).ok_or_else(|| HResult::new(hr))
}
}
#[allow(non_snake_case)]
pub fn put_DaysInterval(&mut self, v: SHORT) -> Result<(), HResult> {
unsafe {
com_call!(self.0, IDailyTrigger::put_DaysInterval(v))?;
}
Ok(())
}
}
pub struct ExecAction(ComRef<IExecAction>);
impl ExecAction {
to_os_str_putter!(IExecAction::put_Path, &Path);
to_os_str_putter!(IExecAction::put_WorkingDirectory, &Path);
#[allow(non_snake_case)]
pub fn put_Arguments(&mut self, args: &[OsString]) -> Result<(), HResult> {
// based on `make_command_line()` from libstd
// https://github.com/rust-lang/rust/blob/37ff5d388f8c004ca248adb635f1cc84d347eda0/src/libstd/sys/windows/process.rs#L457
let mut s = Vec::new();
fn append_arg(cmd: &mut Vec<u16>, arg: &OsStr) -> Result<(), HResult> {
cmd.push('"' as u16);
let mut backslashes: usize = 0;
for x in arg.encode_wide() {
if x == 0 {
return Err(HResult::from(Win32Error::new(ERROR_BAD_ARGUMENTS))
.file_line(file!(), line!()));
}
if x == '\\' as u16 {
backslashes += 1;
} else {
if x == '"' as u16 {
// Add n+1 backslashes for a total of 2n+1 before internal '"'.
cmd.extend((0..=backslashes).map(|_| '\\' as u16));
}
backslashes = 0;
}
cmd.push(x);
}
// Add n backslashes for a total of 2n before ending '"'.
cmd.extend((0..backslashes).map(|_| '\\' as u16));
cmd.push('"' as u16);
Ok(())
}
for arg in args {
if !s.is_empty() {
s.push(' ' as u16);
}
// always quote args
append_arg(&mut s, arg.as_ref())?;
}
let args = BString::from_slice(s).map_err(|e| e.file_line(file!(), line!()))?;
unsafe {
com_call!(self.0, IExecAction::put_Arguments(args.as_raw_ptr()))?;
}
Ok(())
}
}
pub struct RegisteredTask(ComRef<IRegisteredTask>);
impl RegisteredTask {
pub fn set_sd(&mut self, sddl: &BString) -> Result<(), HResult> {
unsafe {
com_call!(
self.0,
IRegisteredTask::SetSecurityDescriptor(
sddl.as_raw_ptr(),
0, // flags (none)
)
)?;
}
Ok(())
}
pub fn get_definition(&mut self) -> Result<TaskDefinition, HResult> {
unsafe { com_call_getter!(|tc| self.0, IRegisteredTask::get_Definition(tc)) }
.map(TaskDefinition)
}
pub fn run(&self) -> Result<(), HResult> {
self.run_impl(Option::<&OsStr>::None)?;
Ok(())
}
fn run_impl(&self, param: Option<impl AsRef<OsStr>>) -> Result<ComRef<IRunningTask>, HResult> {
// Running with parameters isn't currently exposed.
// param can also be an array of strings, but that is not supported here
let param = if let Some(p) = param {
Some(try_to_bstring!(p)?)
} else {
None
};
unsafe {
com_call_getter!(
|rt| self.0,
IRegisteredTask::Run(param.as_ref().as_raw_variant(), rt)
)
}
}
}