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}