|
|
|
|
@ -204,19 +204,34 @@ fn get_wintun_install_path() -> String {
|
|
|
|
|
String::new()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A `Command` for a console program, with the console window suppressed.
|
|
|
|
|
///
|
|
|
|
|
/// The GUI is a windowed-subsystem binary, so every console child it spawns
|
|
|
|
|
/// pops up a console window for as long as that child runs. With `reg`,
|
|
|
|
|
/// `tasklist` and `schtasks` all being invoked from here, that surfaced as
|
|
|
|
|
/// windows flashing on screen — worst while polling for the scheduled task,
|
|
|
|
|
/// which could spawn twenty of them in a row.
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn quiet_command(program: &str) -> std::process::Command {
|
|
|
|
|
use std::os::windows::process::CommandExt;
|
|
|
|
|
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
|
|
|
|
let mut cmd = std::process::Command::new(program);
|
|
|
|
|
cmd.creation_flags(CREATE_NO_WINDOW);
|
|
|
|
|
cmd
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Sets or removes the app from Windows startup (HKCU\...\Run).
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
fn set_autostart(enable: bool) -> Result<(), String> {
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
{
|
|
|
|
|
use std::process::Command;
|
|
|
|
|
let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run";
|
|
|
|
|
let app_name = "OSTP";
|
|
|
|
|
if enable {
|
|
|
|
|
let exe = std::env::current_exe()
|
|
|
|
|
.map_err(|e| format!("Cannot get exe path: {}", e))?;
|
|
|
|
|
let exe_str = format!("\"{}\"", exe.to_string_lossy());
|
|
|
|
|
let out = Command::new("reg")
|
|
|
|
|
let out = quiet_command("reg")
|
|
|
|
|
.args(["add", key, "/v", app_name, "/t", "REG_SZ", "/d", &exe_str, "/f"])
|
|
|
|
|
.output()
|
|
|
|
|
.map_err(|e| format!("reg add failed: {}", e))?;
|
|
|
|
|
@ -224,7 +239,7 @@ fn set_autostart(enable: bool) -> Result<(), String> {
|
|
|
|
|
return Err(String::from_utf8_lossy(&out.stderr).to_string());
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let _ = Command::new("reg")
|
|
|
|
|
let _ = quiet_command("reg")
|
|
|
|
|
.args(["delete", key, "/v", app_name, "/f"])
|
|
|
|
|
.output();
|
|
|
|
|
}
|
|
|
|
|
@ -275,9 +290,8 @@ fn linux_autostart_path() -> Option<PathBuf> {
|
|
|
|
|
fn get_autostart() -> bool {
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
{
|
|
|
|
|
use std::process::Command;
|
|
|
|
|
let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run";
|
|
|
|
|
let out = Command::new("reg")
|
|
|
|
|
let out = quiet_command("reg")
|
|
|
|
|
.args(["query", key, "/v", "OSTP"])
|
|
|
|
|
.output();
|
|
|
|
|
if let Ok(o) = out {
|
|
|
|
|
@ -298,8 +312,7 @@ fn get_autostart() -> bool {
|
|
|
|
|
fn list_running_processes() -> Vec<String> {
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
{
|
|
|
|
|
use std::process::Command;
|
|
|
|
|
if let Ok(out) = Command::new("tasklist")
|
|
|
|
|
if let Ok(out) = quiet_command("tasklist")
|
|
|
|
|
.args(["/FO", "CSV", "/NH"])
|
|
|
|
|
.output()
|
|
|
|
|
{
|
|
|
|
|
@ -836,15 +849,74 @@ fn xml_escape(s: &str) -> String {
|
|
|
|
|
.replace('\'', "'")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether the elevated-launch Scheduled Task already exists.
|
|
|
|
|
/// Reverse of [`xml_escape`]. `&` must be undone last or `&lt;` would
|
|
|
|
|
/// come back as `<`.
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn helper_task_exists() -> bool {
|
|
|
|
|
use std::process::Command;
|
|
|
|
|
Command::new("schtasks")
|
|
|
|
|
.args(["/Query", "/TN", HELPER_TASK_NAME])
|
|
|
|
|
fn xml_unescape(s: &str) -> String {
|
|
|
|
|
s.replace(""", "\"")
|
|
|
|
|
.replace("'", "'")
|
|
|
|
|
.replace("<", "<")
|
|
|
|
|
.replace(">", ">")
|
|
|
|
|
.replace("&", "&")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The exe path currently baked into the registered task, if any.
|
|
|
|
|
///
|
|
|
|
|
/// Queried as XML rather than `/FO LIST /V`: the list format's field labels are
|
|
|
|
|
/// localized (on a Russian Windows "Task To Run" is "Задача для запуска"),
|
|
|
|
|
/// whereas XML tag names are fixed. schtasks writes UTF-16LE with a BOM here,
|
|
|
|
|
/// but tolerate UTF-8 in case that ever changes.
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn helper_task_command() -> Option<String> {
|
|
|
|
|
let out = quiet_command("schtasks")
|
|
|
|
|
.args(["/Query", "/TN", HELPER_TASK_NAME, "/XML"])
|
|
|
|
|
.output()
|
|
|
|
|
.map(|o| o.status.success())
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
.ok()?;
|
|
|
|
|
if !out.status.success() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let text = if out.stdout.starts_with(&[0xFF, 0xFE]) {
|
|
|
|
|
let units: Vec<u16> = out.stdout[2..]
|
|
|
|
|
.chunks_exact(2)
|
|
|
|
|
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
|
|
|
|
.collect();
|
|
|
|
|
String::from_utf16_lossy(&units)
|
|
|
|
|
} else {
|
|
|
|
|
String::from_utf8_lossy(&out.stdout).into_owned()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let start = text.find("<Command>")? + "<Command>".len();
|
|
|
|
|
let end = text[start..].find("</Command>")? + start;
|
|
|
|
|
Some(xml_unescape(text[start..end].trim()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether a task is registered AND still points at the exe we are about to run.
|
|
|
|
|
///
|
|
|
|
|
/// The path matters as much as the name. A task registered by a dev build (or
|
|
|
|
|
/// by an install that has since moved) keeps its original `<Command>`, and
|
|
|
|
|
/// `schtasks /Run` reports success merely for *accepting* the request — a task
|
|
|
|
|
/// whose exe no longer exists fails asynchronously and silently. Trusting the
|
|
|
|
|
/// name alone therefore bought a 60-second "Timeout connecting to helper" on
|
|
|
|
|
/// every single connect, permanently, until the task was deleted by hand.
|
|
|
|
|
/// Re-registering costs one consent prompt and fixes it for good.
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn helper_task_matches(exe: &std::path::Path) -> bool {
|
|
|
|
|
let Some(registered) = helper_task_command() else {
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
let registered = registered.trim().trim_matches('"');
|
|
|
|
|
|
|
|
|
|
// Canonicalize both sides when possible so `..`, short 8.3 names and
|
|
|
|
|
// casing differences do not read as a mismatch. A missing file cannot be
|
|
|
|
|
// canonicalized — which is itself a mismatch worth re-registering over.
|
|
|
|
|
match (
|
|
|
|
|
std::fs::canonicalize(registered),
|
|
|
|
|
std::fs::canonicalize(exe),
|
|
|
|
|
) {
|
|
|
|
|
(Ok(a), Ok(b)) => a == b,
|
|
|
|
|
_ => registered.eq_ignore_ascii_case(&exe.display().to_string()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Register the Scheduled Task. This is the ONLY step that needs elevation, and
|
|
|
|
|
@ -917,26 +989,54 @@ fn install_helper_task(exe: &std::path::Path) -> anyhow::Result<()> {
|
|
|
|
|
|
|
|
|
|
// Registering a HighestAvailable task is itself privileged: this is the one
|
|
|
|
|
// prompt, and it happens once per machine.
|
|
|
|
|
let schtasks = std::path::PathBuf::from("schtasks.exe");
|
|
|
|
|
let params = format!(
|
|
|
|
|
"/Create /TN \"{}\" /XML \"{}\" /F",
|
|
|
|
|
HELPER_TASK_NAME,
|
|
|
|
|
xml_path.display()
|
|
|
|
|
//
|
|
|
|
|
// Elevate through PowerShell's Start-Process -Wait rather than
|
|
|
|
|
// ShellExecuteW. ShellExecuteW returns as soon as the elevated process is
|
|
|
|
|
// LAUNCHED, so the XML below was being deleted while schtasks was still
|
|
|
|
|
// starting up — registration then failed, leaving the user with a consent
|
|
|
|
|
// prompt that accomplished nothing, followed by a second prompt from the
|
|
|
|
|
// fallback path. -Wait makes the deletion safe and lets the exit code be
|
|
|
|
|
// checked instead of guessed at by polling.
|
|
|
|
|
//
|
|
|
|
|
// ArgumentList takes an array, so the task name and XML path never need
|
|
|
|
|
// quoting or escaping through a command line, only PowerShell's own
|
|
|
|
|
// single-quote doubling.
|
|
|
|
|
let ps = format!(
|
|
|
|
|
"$p = Start-Process -FilePath 'schtasks.exe' -Verb RunAs -Wait -PassThru \
|
|
|
|
|
-WindowStyle Hidden -ArgumentList @('/Create','/TN','{}','/XML','{}','/F'); \
|
|
|
|
|
exit $p.ExitCode",
|
|
|
|
|
ps_quote(HELPER_TASK_NAME),
|
|
|
|
|
ps_quote(&xml_path.display().to_string()),
|
|
|
|
|
);
|
|
|
|
|
let result = shell_execute_elevated(&schtasks, ¶ms);
|
|
|
|
|
// Best-effort cleanup; schtasks may still be reading it, so ignore errors.
|
|
|
|
|
let _ = std::fs::remove_file(&xml_path);
|
|
|
|
|
result?;
|
|
|
|
|
|
|
|
|
|
// schtasks runs asynchronously through ShellExecute; wait briefly for the
|
|
|
|
|
// task to appear rather than reporting success before it exists.
|
|
|
|
|
for _ in 0..20 {
|
|
|
|
|
if helper_task_exists() {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
std::thread::sleep(std::time::Duration::from_millis(250));
|
|
|
|
|
let status = quiet_command("powershell")
|
|
|
|
|
.args(["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", &ps])
|
|
|
|
|
.status();
|
|
|
|
|
|
|
|
|
|
// schtasks has exited by now, so this is safe.
|
|
|
|
|
let _ = std::fs::remove_file(&xml_path);
|
|
|
|
|
|
|
|
|
|
match status {
|
|
|
|
|
Ok(s) if s.success() => {}
|
|
|
|
|
Ok(s) => anyhow::bail!(
|
|
|
|
|
"registering the scheduled task failed (exit code {:?}). A declined consent prompt \
|
|
|
|
|
reports 1223.",
|
|
|
|
|
s.code()
|
|
|
|
|
),
|
|
|
|
|
Err(e) => anyhow::bail!("could not run powershell to register the task: {e}"),
|
|
|
|
|
}
|
|
|
|
|
anyhow::bail!("the scheduled task did not appear after the elevation prompt (it may have been declined)")
|
|
|
|
|
|
|
|
|
|
if helper_task_matches(exe) {
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
anyhow::bail!("schtasks reported success but the task does not point at {}", exe.display())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Escape a value for embedding in a PowerShell single-quoted string.
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn ps_quote(s: &str) -> String {
|
|
|
|
|
s.replace('\'', "''")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
@ -954,14 +1054,13 @@ fn launch_as_admin(exe: &std::path::PathBuf, token: &str, port: u16) -> anyhow::
|
|
|
|
|
let wrote_args = std::fs::write(&args_file, payload.to_string()).is_ok();
|
|
|
|
|
|
|
|
|
|
if wrote_args {
|
|
|
|
|
if !helper_task_exists() {
|
|
|
|
|
if !helper_task_matches(exe) {
|
|
|
|
|
if let Err(e) = install_helper_task(exe) {
|
|
|
|
|
eprintln!("[OSTP] could not register the helper task ({e}); falling back to a direct elevated launch");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if helper_task_exists() {
|
|
|
|
|
use std::process::Command;
|
|
|
|
|
let run = Command::new("schtasks")
|
|
|
|
|
if helper_task_matches(exe) {
|
|
|
|
|
let run = quiet_command("schtasks")
|
|
|
|
|
.args(["/Run", "/TN", HELPER_TASK_NAME])
|
|
|
|
|
.output();
|
|
|
|
|
match run {
|
|
|
|
|
@ -1035,60 +1134,6 @@ fn launch_as_admin_direct(exe: &std::path::PathBuf, token: &str, port: u16) -> a
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Run `exe` elevated with `params`, raising the UAC prompt.
|
|
|
|
|
///
|
|
|
|
|
/// Shared by the fallback launch path and by the one-time task registration, so
|
|
|
|
|
/// both report a declined prompt the same way instead of ShellExecuteW's
|
|
|
|
|
/// pseudo-HINSTANCE being interpreted twice.
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn shell_execute_elevated(exe: &std::path::Path, params: &str) -> anyhow::Result<()> {
|
|
|
|
|
use std::ffi::OsStr;
|
|
|
|
|
use std::os::windows::ffi::OsStrExt;
|
|
|
|
|
use std::ptr::null_mut;
|
|
|
|
|
|
|
|
|
|
let exe_wstr: Vec<u16> = exe.as_os_str().encode_wide().chain(Some(0)).collect();
|
|
|
|
|
let verb_wstr: Vec<u16> = OsStr::new("runas").encode_wide().chain(Some(0)).collect();
|
|
|
|
|
let params_wstr: Vec<u16> = OsStr::new(params).encode_wide().chain(Some(0)).collect();
|
|
|
|
|
|
|
|
|
|
#[link(name = "shell32")]
|
|
|
|
|
extern "system" {
|
|
|
|
|
fn ShellExecuteW(h: *mut std::ffi::c_void, op: *const u16, f: *const u16, p: *const u16, d: *const u16, s: i32) -> isize;
|
|
|
|
|
}
|
|
|
|
|
#[link(name = "kernel32")]
|
|
|
|
|
extern "system" {
|
|
|
|
|
fn GetLastError() -> u32;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let cwd_path = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("."));
|
|
|
|
|
let dir_wstr: Vec<u16> = cwd_path
|
|
|
|
|
.parent()
|
|
|
|
|
.unwrap_or(std::path::Path::new("."))
|
|
|
|
|
.as_os_str()
|
|
|
|
|
.encode_wide()
|
|
|
|
|
.chain(Some(0))
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
let ret = unsafe {
|
|
|
|
|
ShellExecuteW(null_mut(), verb_wstr.as_ptr(), exe_wstr.as_ptr(), params_wstr.as_ptr(), dir_wstr.as_ptr(), 1)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 1223 is ERROR_CANCELLED, which lands in the ">32 means success" range —
|
|
|
|
|
// see the note in launch_as_admin_direct.
|
|
|
|
|
if ret == 1223 {
|
|
|
|
|
anyhow::bail!("UAC elevation was denied.");
|
|
|
|
|
}
|
|
|
|
|
if ret <= 32 {
|
|
|
|
|
let win_err = unsafe { GetLastError() };
|
|
|
|
|
anyhow::bail!(
|
|
|
|
|
"Failed to request UAC elevation (ShellExecuteW ret={}, GetLastError={}, path={})",
|
|
|
|
|
ret,
|
|
|
|
|
win_err,
|
|
|
|
|
exe.display()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "linux")]
|
|
|
|
|
fn launch_as_admin(exe: &PathBuf, token: &str, port: u16) -> Result<()> {
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
|
|