virtual_desktop_manager/auto_start.rs
1//! Auto start using the Windows Task Scheduler.
2
3use std::{
4 any::TypeId, env::current_exe, ffi::OsStr, os::windows::process::CommandExt, process::Command,
5 rc::Rc, sync::Arc, time::Duration,
6};
7
8use crate::{
9 dynamic_gui::DynamicUiHooks,
10 settings::{AutoStart, UiSettings},
11 tray::{SystemTray, TrayPlugin},
12};
13
14pub fn change_install(should_install: bool) -> Result<(), String> {
15 // Note: Task Scheduler paths must use backslashes (but runas can't
16 // escape them correctly for schtasks, so don't use them)
17 let task_name = "Lej77's VirtualDesktopManager - Elevated Auto Start".to_string();
18 let was_installed = is_installed(&task_name)
19 .map_err(|e| format!("Failed to check if elevated auto start was installed: {e}"))?;
20
21 if was_installed == should_install {
22 return Ok(());
23 }
24
25 if should_install {
26 let exe_path =
27 current_exe().map_err(|e| format!("failed to resolve the executable's path: {e}"))?;
28 install(&task_name, exe_path.as_ref())
29 .map_err(|e| format!("Failed to install elevated auto start: {e}"))?;
30 } else {
31 uninstall(&task_name)
32 .map_err(|e| format!("Failed to uninstall elevated auto start: {e}"))?;
33 }
34
35 // Wait for changes to be applied:
36 std::thread::sleep(Duration::from_millis(2000));
37
38 let was_installed = is_installed(&task_name)
39 .map_err(|e| format!("Failed to check if elevated auto start was installed: {e}"))?;
40 if was_installed == should_install {
41 Ok(())
42 } else {
43 Err(format!(
44 "failed to {} the task \"{task_name}\" {} the Task Scheduler",
45 if should_install { "create" } else { "remove" },
46 if should_install { "in" } else { "from" }
47 ))
48 }
49}
50
51pub fn is_installed(task_name: &str) -> Result<bool, String> {
52 let output = Command::new("schtasks")
53 .args(["/Query", "/TN"])
54 .arg(task_name)
55 // Hide console window:
56 // https://stackoverflow.com/questions/6371149/what-is-the-difference-between-detach-process-and-create-no-window-process-creat
57 // https://learn.microsoft.com/sv-se/windows/win32/procthread/process-creation-flags?redirectedfrom=MSDN
58 .creation_flags(/*DETACHED_PROCESS*/ 0x00000008)
59 .output()
60 .map_err(|e| format!("failed to run schtasks: {e}"))?;
61 match output.status.code() {
62 Some(0) => Ok(true),
63 Some(1) => Ok(false),
64 code => Err(format!(
65 "failed to check if the task \"{task_name}\" existed in the Task Scheduler{}\n\nStderr:{}",
66 if let Some(code) = code {
67 format!(" (exit code: {code})")
68 } else {
69 "".to_string()
70 },
71 String::from_utf8_lossy(&output.stderr)
72 )),
73 }
74}
75
76pub fn install(task_name: &str, program_path: &OsStr) -> Result<(), String> {
77 // 1. Creating a task that uses the `Highest` `RunLevel` will fail if we
78 // don't have admin rights so we run this command with sudo.
79 // 2. We use "powershell" instead of "schtasks" to create the task since
80 // some task settings aren't exposed as cli flags for "schtasks".
81 // - The settings in question are:
82 // - The task is terminated after 3 days
83 // - The task is only started if the PC is connected to a power
84 // supply.
85 // - Another workaround would be to use "schtasks" XML import option.
86 // - This would require writing a temp file that included the path
87 // to the program that should be started.
88 //
89 // Info about powershell code:
90 // https://learn.microsoft.com/en-us/powershell/module/scheduledtasks/register-scheduledtask?view=windowsserver2022-ps
91 // https://stackoverflow.com/questions/2157554/how-to-handle-command-line-arguments-in-powershell
92 let _status = runas::Command::new("powershell")
93 .arg("-NoProfile")
94 .arg("-NonInteractive")
95 .arg("-WindowStyle")
96 .arg("Hidden")
97 .arg("-Command")
98 // Inline the powershell script that we want to run (alternatively
99 // we could store the code as a file and pass a path to it, but
100 // passing the code directly makes it easier to inspect in the UAC
101 // prompt):
102 .arg(format!(
103 "& {{{}}}",
104 include_str!("./install-elevated-autostart.ps1")
105 ))
106 // Task name:
107 .arg(format!("\"{task_name}\""))
108 // Path to started program:
109 .arg(
110 // If path has spaces then it must be surrounded by quotes,
111 // otherwise anything after the first space will be interpreted
112 // as arguments to the started program:
113 format!(
114 "\"{}\"",
115 program_path
116 .to_str()
117 .ok_or("program path wasn't valid UTF-8")?
118 // schtasks doesn't handle the escaped backslashes
119 // correctly so avoid them:
120 .replace('\\', "/")
121 ),
122 )
123 // Task description:
124 .arg("\"Start Virtual Desktop Manager at startup\"")
125 // Show the admin prompt:
126 .gui(true)
127 // But hide the created schtasks window:
128 .show(false)
129 .status()
130 .map_err(|e| format!("failed to start \"powershell\": {e}"))?;
131 Ok(())
132 // Status code is always -1?
133 // See: https://github.com/mitsuhiko/rust-runas/issues/13
134 // Related to refactor away from C glue code in:
135 // https://github.com/mitsuhiko/rust-runas/commit/220624592f8202107592b83c943aad73bd3142b0
136 /*
137 if status.success() {
138 Ok(())
139 } else {
140 Err(format!(
141 "failed to create the task \"{task_name}\" in the Task Scheduler{}",
142 if let Some(code) = status.code() {
143 format!(" (exit code: {code})")
144 } else {
145 "".to_string()
146 }
147 ))
148 }
149 */
150}
151
152pub fn uninstall(task_name: &str) -> Result<(), String> {
153 if task_name.contains('*') {
154 return Err(
155 "don't use * inside task names, they will be interpreted as wildcards".to_string(),
156 );
157 }
158 let _status = runas::Command::new("schtasks")
159 .arg("/Delete")
160 // Task name:
161 .arg("/TN")
162 .arg(task_name)
163 // Force: skips "are you sure" prompt:
164 .arg("/F")
165 // Show the admin prompt:
166 .gui(true)
167 // But hide the created schtasks window:
168 .show(false)
169 .status()
170 .map_err(|e| format!("failed to run schtasks: {e}"))?;
171 Ok(())
172 // Status code is always -1?
173 /*
174 if status.success() {
175 Ok(())
176 } else {
177 Err(format!(
178 "failed to delete the task \"{task_name}\" in the Task Scheduler{}",
179 if let Some(code) = status.code() {
180 format!(" (exit code: {code})")
181 } else {
182 "".to_string()
183 }
184 ))
185 }
186 */
187}
188
189/// This plugin tracks UI settings.
190#[derive(nwd::NwgPartial, Default)]
191pub struct AutoStartPlugin {}
192impl AutoStartPlugin {
193 fn update_installed(&self, tray_ui: &SystemTray) {
194 if cfg!(debug_assertions) {
195 return;
196 }
197 // TODO(perf): do this in a background thread.
198 // TODO(feat): support non elevated auto start.
199 let res = change_install(tray_ui.settings().get().auto_start != AutoStart::Disabled);
200 if let Err(e) = res {
201 tray_ui.show_notification("Virtual Desktop Manager Error", &e);
202 }
203 }
204}
205impl DynamicUiHooks<SystemTray> for AutoStartPlugin {
206 fn before_partial_build(
207 &mut self,
208 _tray_ui: &Rc<SystemTray>,
209 _should_build: &mut bool,
210 ) -> Option<(nwg::ControlHandle, TypeId)> {
211 None
212 }
213 fn after_partial_build(&mut self, tray_ui: &Rc<SystemTray>) {
214 self.update_installed(tray_ui);
215 }
216}
217impl TrayPlugin for AutoStartPlugin {
218 fn on_settings_changed(
219 &self,
220 tray_ui: &Rc<SystemTray>,
221 prev: &Arc<UiSettings>,
222 new: &Arc<UiSettings>,
223 ) {
224 if prev.auto_start != new.auto_start {
225 self.update_installed(tray_ui);
226 }
227 }
228}