use crate::context::Context;
use crate::error::{Error, Result};
use crate::host::task::{FOLDER, Registered, Service, from_ole_date};
use crate::service::Permit;
use windows::Win32::Foundation::RPC_E_CHANGED_MODE;
use windows::Win32::System::Com::{
CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx, CoUninitialize,
};
use windows::Win32::System::TaskScheduler::{
ITaskFolder, ITaskService, TASK_CREATE_OR_UPDATE, TASK_LOGON_INTERACTIVE_TOKEN, TaskScheduler,
};
use windows::Win32::System::Variant::VARIANT;
use windows::core::BSTR;
#[derive(Debug)]
pub(super) struct System;
const NOT_FOUND: [u32; 2] = [0x8007_0002, 0x8007_0003];
const ALREADY_THERE: u32 = 0x8007_00B7;
const ASKED_IN_A_TEST: &str = "a build for tests asked Task Scheduler itself";
impl Service for System {
fn read(&self, ctx: &Context, name: &str) -> Result<Option<Registered>> {
if cfg!(any(test, feature = "test-support")) {
return Ok(None);
}
let (name, zone) = (name.to_owned(), ctx.zone());
in_the_mta(move |service| {
let Some(folder) = folder(service)? else {
return Ok(None);
};
unsafe {
let task = match folder.GetTask(&BSTR::from(name.as_str())) {
Ok(task) => task,
Err(e) if not_found(&e) => return Ok(None),
Err(e) => return Err(e),
};
Ok(Some(Registered {
xml: task.Xml()?.to_string(),
last_run: task
.LastRunTime()
.ok()
.and_then(|date| from_ole_date(date, &zone)),
last_result: task.LastTaskResult().ok(),
}))
}
})
}
fn register(&self, _: Permit, _ctx: &Context, name: &str, xml: &str) -> Result<()> {
if cfg!(any(test, feature = "test-support")) {
return Err(Error::ScheduleRefused {
detail: ASKED_IN_A_TEST.into(),
});
}
let (name, xml) = (name.to_owned(), xml.to_owned());
in_the_mta(move |service| {
let folder = made_folder(service)?;
unsafe {
folder.RegisterTask(
&BSTR::from(name.as_str()),
&BSTR::from(xml.as_str()),
TASK_CREATE_OR_UPDATE.0,
&VARIANT::default(),
&VARIANT::default(),
TASK_LOGON_INTERACTIVE_TOKEN,
&VARIANT::default(),
)
}
.map(drop)
})
}
fn delete(&self, _: Permit, _ctx: &Context, name: &str) -> Result<()> {
if cfg!(any(test, feature = "test-support")) {
return Err(Error::ScheduleRefused {
detail: ASKED_IN_A_TEST.into(),
});
}
let name = name.to_owned();
in_the_mta(move |service| {
let Some(folder) = folder(service)? else {
return Ok(());
};
unsafe {
match folder.DeleteTask(&BSTR::from(name.as_str()), 0) {
Err(e) if !not_found(&e) => return Err(e),
_ => {}
}
let _ = service
.GetFolder(&BSTR::from(r"\"))
.and_then(|root| root.DeleteFolder(&BSTR::from(&FOLDER[1..]), 0));
}
Ok(())
})
}
}
fn refused(detail: String) -> Error {
Error::ScheduleRefused { detail }
}
fn not_found(e: &windows::core::Error) -> bool {
NOT_FOUND.contains(&(e.code().0 as u32))
}
fn in_the_mta<T: Send>(
work: impl FnOnce(&ITaskService) -> windows::core::Result<T> + Send,
) -> Result<T> {
let said = |e: windows::core::Error| refused(format!("Task Scheduler said {e}"));
std::thread::scope(|scope| {
scope
.spawn(|| {
let joined = unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) };
if joined.is_err() && joined != RPC_E_CHANGED_MODE {
return Err(said(joined.into()));
}
let done = connected().and_then(|service| work(&service)).map_err(said);
if joined.is_ok() {
unsafe { CoUninitialize() };
}
done
})
.join()
.unwrap_or_else(|_| Err(refused("Task Scheduler's thread panicked".into())))
})
}
fn connected() -> windows::core::Result<ITaskService> {
unsafe {
let service: ITaskService = CoCreateInstance(&TaskScheduler, None, CLSCTX_INPROC_SERVER)?;
service.Connect(
&VARIANT::default(),
&VARIANT::default(),
&VARIANT::default(),
&VARIANT::default(),
)?;
Ok(service)
}
}
fn folder(service: &ITaskService) -> windows::core::Result<Option<ITaskFolder>> {
match unsafe { service.GetFolder(&BSTR::from(FOLDER)) } {
Ok(folder) => Ok(Some(folder)),
Err(e) if not_found(&e) => Ok(None),
Err(e) => Err(e),
}
}
fn made_folder(service: &ITaskService) -> windows::core::Result<ITaskFolder> {
unsafe {
if let Ok(folder) = service.GetFolder(&BSTR::from(FOLDER)) {
return Ok(folder);
}
let made = service
.GetFolder(&BSTR::from(r"\"))?
.CreateFolder(&BSTR::from(&FOLDER[1..]), &VARIANT::default());
match made {
Err(e) if e.code().0 as u32 == ALREADY_THERE => service.GetFolder(&BSTR::from(FOLDER)),
made => made,
}
}
}