зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
b154619cbb
Коммит
4b591f8eae
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче