595 lines
24 KiB
Rust
595 lines
24 KiB
Rust
//! What differs per operating system: directories, file permissions, keeping the machine awake, opening a URL,
|
|
//! starting at login, and stopping a child process gracefully.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
/// The absolute path of a system helper (R4.3.3: never a bare name on PATH). Windows: System32 (and NVIDIA's own
|
|
/// folder for nvidia-smi); macOS: /usr/bin, /usr/sbin, /bin. Unknown names fall back to the bare name.
|
|
pub fn tool(name: &str) -> PathBuf {
|
|
#[cfg(windows)]
|
|
{
|
|
let root = std::env::var("SystemRoot").unwrap_or_else(|_| "C:\\Windows".into());
|
|
let sys = format!("{root}\\System32");
|
|
let p = match name {
|
|
"powershell" => format!("{sys}\\WindowsPowerShell\\v1.0\\powershell.exe"),
|
|
"cmd" | "curl" | "reg" | "icacls" | "taskkill" | "w32tm" | "net" | "tar" | "wsl" => format!("{sys}\\{name}.exe"),
|
|
"nvidia-smi" => {
|
|
let pf = std::env::var("ProgramFiles").unwrap_or_else(|_| "C:\\Program Files".into());
|
|
let a = format!("{pf}\\NVIDIA Corporation\\NVSMI\\nvidia-smi.exe");
|
|
let b = format!("{sys}\\nvidia-smi.exe");
|
|
if Path::new(&a).is_file() { a } else { b }
|
|
}
|
|
_ => name.to_string(),
|
|
};
|
|
PathBuf::from(p)
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let p = match name {
|
|
"curl" | "osascript" | "xattr" | "open" | "caffeinate" | "hdiutil" | "ditto" | "nohup" | "pgrep" | "pkill" | "shasum" | "xcrun" => format!("/usr/bin/{name}"),
|
|
"sntp" | "scutil" | "system_profiler" | "sysctl" => format!("/usr/sbin/{name}"),
|
|
"bash" | "sh" => format!("/bin/{name}"),
|
|
_ => name.to_string(),
|
|
};
|
|
PathBuf::from(p)
|
|
}
|
|
#[cfg(not(any(windows, target_os = "macos")))]
|
|
{
|
|
for dir in ["/usr/bin", "/bin", "/usr/sbin", "/usr/local/bin"] {
|
|
let p = Path::new(dir).join(name);
|
|
if p.is_file() {
|
|
return p;
|
|
}
|
|
}
|
|
PathBuf::from(name)
|
|
}
|
|
}
|
|
|
|
/// Strips the dashboard token from a line: "/t/<32 hex>/" becomes "/t/<token>/" (R4.3.8: the token is never logged
|
|
/// and the logs are uploaded).
|
|
pub fn redact(s: &str) -> String {
|
|
let mut out = String::with_capacity(s.len());
|
|
let mut rest = s;
|
|
while let Some(i) = rest.find("/t/") {
|
|
out.push_str(&rest[..i + 3]);
|
|
let after = &rest[i + 3..];
|
|
let hex_len = after.chars().take_while(|c| c.is_ascii_hexdigit()).count();
|
|
if hex_len >= 16 {
|
|
out.push_str("<token>");
|
|
rest = &after[hex_len..];
|
|
} else {
|
|
rest = after;
|
|
}
|
|
}
|
|
out.push_str(rest);
|
|
out
|
|
}
|
|
|
|
/// True when a line still carries something that looks like the dashboard token (the upload guard).
|
|
pub fn carries_token(s: &str) -> bool {
|
|
let mut rest = s;
|
|
while let Some(i) = rest.find("/t/") {
|
|
let after = &rest[i + 3..];
|
|
if after.chars().take_while(|c| c.is_ascii_hexdigit()).count() >= 16 {
|
|
return true;
|
|
}
|
|
rest = after;
|
|
}
|
|
false
|
|
}
|
|
|
|
pub fn unix_now() -> u64 {
|
|
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
|
|
}
|
|
|
|
pub fn unix_now_f() -> f64 {
|
|
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0)
|
|
}
|
|
|
|
fn home() -> PathBuf {
|
|
#[cfg(windows)]
|
|
{
|
|
if let Some(p) = std::env::var_os("USERPROFILE") {
|
|
return PathBuf::from(p);
|
|
}
|
|
}
|
|
std::env::var_os("HOME").map(PathBuf::from).unwrap_or_else(|| PathBuf::from("."))
|
|
}
|
|
|
|
/// The root the app writes under. macOS: ~/Library/Application Support/Igneum. Windows: %LOCALAPPDATA%\igneum
|
|
/// (the same folder today's launchers use, so an existing devnet-v4 database is reused).
|
|
pub fn data_root() -> PathBuf {
|
|
if let Some(p) = std::env::var_os("IGNEUM_APP_DATA") {
|
|
return PathBuf::from(p);
|
|
}
|
|
fixed_data_root()
|
|
}
|
|
|
|
/// The platform's own root, with IGNEUM_APP_DATA ignored: where the Power Helper task (started by Windows with no
|
|
/// environment of ours) reads its command file, so a measurement engine under a scratch root (IGNEUM_APP_DATA set)
|
|
/// writes its task commands here and not under its own root. 6 October 2026: found while planning the unelevated
|
|
/// climb through the task; the elevated runs never crossed it (they set the limits directly).
|
|
pub fn fixed_data_root() -> PathBuf {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
home().join("Library").join("Application Support").join("Igneum")
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
std::env::var_os("LOCALAPPDATA").map(PathBuf::from).unwrap_or_else(|| home().join("AppData").join("Local")).join("igneum")
|
|
}
|
|
#[cfg(not(any(target_os = "macos", windows)))]
|
|
{
|
|
home().join(".igneum")
|
|
}
|
|
}
|
|
|
|
pub fn log_root() -> PathBuf {
|
|
if let Some(p) = std::env::var_os("IGNEUM_APP_LOGS") {
|
|
return PathBuf::from(p);
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
home().join("Library").join("Logs").join("Igneum")
|
|
}
|
|
#[cfg(not(target_os = "macos"))]
|
|
{
|
|
data_root().join("logs")
|
|
}
|
|
}
|
|
|
|
/// The machine's short name, cleaned to [A-Za-z0-9-], for the miner labels (mac-<host>, nvidia-<pc>).
|
|
pub fn host_label() -> String {
|
|
let raw = {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
Command::new(tool("scutil")).args(["--get", "LocalHostName"]).output().ok().and_then(|o| String::from_utf8(o.stdout).ok())
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
std::env::var("COMPUTERNAME").ok()
|
|
}
|
|
#[cfg(not(any(target_os = "macos", windows)))]
|
|
{
|
|
std::fs::read_to_string("/etc/hostname").ok()
|
|
}
|
|
};
|
|
let raw = raw.unwrap_or_default();
|
|
let cleaned: String = raw.trim().chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '-' }).collect();
|
|
let cleaned = cleaned.trim_matches('-').to_string();
|
|
if cleaned.is_empty() {
|
|
if cfg!(windows) { "pc".into() } else { "mac".into() }
|
|
} else {
|
|
cleaned
|
|
}
|
|
}
|
|
|
|
/// Restricts a file (or directory) to the current user.
|
|
pub fn lock_permissions(path: &Path, dir: bool) {
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(if dir { 0o700 } else { 0o600 }));
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
let user = std::env::var("USERNAME").unwrap_or_default();
|
|
if !user.is_empty() {
|
|
let _ = quiet(&mut Command::new(tool("icacls"))).arg(path).args(icacls_lock_args(dir, &user)).output();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The icacls arguments that lock a path to the user. A folder gets an INHERITABLE grant (`user:(OI)(CI)F`) and
|
|
/// NO `/T`: Windows propagates the inheritable entry to every child, existing or future, as `(I)(F)`. Measured on
|
|
/// PC 1, 6 October 2026 (collect ember-acl-2): the old non-inheritable `user:F` cut the folder's inheritance and
|
|
/// left a file COPIED in before the engine started with no entry at all (the measurement engine's settings.json,
|
|
/// machine-id and wallet.json read as nothing, so it ran on defaults with no payout address; its own files, written
|
|
/// after the lock, inherited fine and hid it); the same grant WITH `/T` also left the file empty, because `/T`
|
|
/// re-applies `/inheritance:r` to the file after the propagation and an `(OI)(CI)` entry on a file is inherit-only.
|
|
pub fn icacls_lock_args(dir: bool, user: &str) -> Vec<String> {
|
|
if dir {
|
|
vec!["/inheritance:r".into(), "/grant:r".into(), format!("{user}:(OI)(CI)F")]
|
|
} else {
|
|
vec!["/inheritance:r".into(), "/grant:r".into(), format!("{user}:F")]
|
|
}
|
|
}
|
|
|
|
/// Opens a URL in the default browser (the fallback when no window host runs).
|
|
pub fn open_url(url: &str) {
|
|
#[cfg(target_os = "macos")]
|
|
let _ = Command::new(tool("open")).arg(url).spawn();
|
|
#[cfg(windows)]
|
|
let _ = quiet(&mut Command::new(tool("cmd"))).args(["/c", "start", "", url]).spawn();
|
|
#[cfg(not(any(target_os = "macos", windows)))]
|
|
let _ = Command::new("xdg-open").arg(url).spawn();
|
|
}
|
|
|
|
/// Keeps the machine awake while the engine runs. macOS: caffeinate tied to this process. Windows: the execution state,
|
|
/// which must be refreshed (call `keep_awake_tick` every minute).
|
|
pub struct KeepAwake {
|
|
#[cfg(target_os = "macos")]
|
|
child: Option<std::process::Child>,
|
|
}
|
|
|
|
impl KeepAwake {
|
|
pub fn start() -> KeepAwake {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let child = Command::new(tool("caffeinate")).args(["-dims", "-w", &std::process::id().to_string()]).spawn().ok();
|
|
KeepAwake { child }
|
|
}
|
|
#[cfg(not(target_os = "macos"))]
|
|
{
|
|
keep_awake_tick();
|
|
KeepAwake {}
|
|
}
|
|
}
|
|
pub fn stop(&mut self) {
|
|
#[cfg(target_os = "macos")]
|
|
if let Some(c) = self.child.as_mut() {
|
|
let _ = c.kill();
|
|
let _ = c.wait();
|
|
}
|
|
#[cfg(windows)]
|
|
unsafe {
|
|
SetThreadExecutionState(ES_CONTINUOUS);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
const ES_CONTINUOUS: u32 = 0x8000_0000;
|
|
#[cfg(windows)]
|
|
const ES_SYSTEM_REQUIRED: u32 = 0x0000_0001;
|
|
#[cfg(windows)]
|
|
#[link(name = "kernel32")]
|
|
extern "system" {
|
|
fn SetThreadExecutionState(flags: u32) -> u32;
|
|
}
|
|
|
|
pub fn keep_awake_tick() {
|
|
#[cfg(windows)]
|
|
unsafe {
|
|
SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED);
|
|
}
|
|
}
|
|
|
|
/// Asks a child to stop. Unix: SIGTERM (the node closes its database cleanly). Windows: TerminateProcess through
|
|
/// std (what today's launcher does with taskkill /F).
|
|
pub fn terminate(child: &mut std::process::Child) {
|
|
#[cfg(unix)]
|
|
unsafe {
|
|
libc::kill(child.id() as i32, libc::SIGTERM);
|
|
}
|
|
#[cfg(not(unix))]
|
|
{
|
|
let _ = child.kill();
|
|
}
|
|
}
|
|
|
|
/// The app bundle on macOS (Igneum Miner.app) when the engine runs from inside one.
|
|
pub fn bundle_path() -> Option<PathBuf> {
|
|
let exe = std::env::current_exe().ok()?;
|
|
let macos = exe.parent()?;
|
|
let contents = macos.parent()?;
|
|
if macos.file_name()? == "MacOS" && contents.file_name()? == "Contents" {
|
|
contents.parent().map(|p| p.to_path_buf())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// What a login item should run: the bundle (macOS) or the window host / the engine (Windows).
|
|
fn login_command() -> Vec<String> {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
if let Some(b) = bundle_path() {
|
|
return vec!["/usr/bin/open".into(), "-a".into(), b.to_string_lossy().into_owned()];
|
|
}
|
|
}
|
|
let exe = std::env::current_exe().map(|p| p.to_string_lossy().into_owned()).unwrap_or_default();
|
|
#[cfg(windows)]
|
|
{
|
|
if let Some(dir) = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())) {
|
|
let host = dir.join("Igneum Miner.exe");
|
|
if host.exists() {
|
|
return vec![host.to_string_lossy().into_owned()];
|
|
}
|
|
}
|
|
}
|
|
vec![exe]
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn launch_agent_path() -> PathBuf {
|
|
home().join("Library").join("LaunchAgents").join("network.igneum.miner.plist")
|
|
}
|
|
|
|
pub fn set_start_at_login(on: bool) -> Result<(), String> {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let path = launch_agent_path();
|
|
if on {
|
|
let args: String = login_command()
|
|
.iter()
|
|
.map(|a| format!(" <string>{}</string>\n", a.replace('&', "&").replace('<', "<")))
|
|
.collect();
|
|
let plist = format!(
|
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key>\n <string>network.igneum.miner</string>\n <key>ProgramArguments</key>\n <array>\n{args} </array>\n <key>RunAtLoad</key>\n <true/>\n</dict>\n</plist>\n"
|
|
);
|
|
if let Some(d) = path.parent() {
|
|
std::fs::create_dir_all(d).map_err(|e| e.to_string())?;
|
|
}
|
|
std::fs::write(&path, plist).map_err(|e| e.to_string())?;
|
|
} else if path.exists() {
|
|
std::fs::remove_file(&path).map_err(|e| e.to_string())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run";
|
|
let out = if on {
|
|
let cmd = login_command().iter().map(|a| format!("\"{a}\"")).collect::<Vec<_>>().join(" ");
|
|
quiet(&mut Command::new(tool("reg"))).args(["add", key, "/v", "Igneum Miner", "/t", "REG_SZ", "/d", &cmd, "/f"]).output()
|
|
} else {
|
|
quiet(&mut Command::new(tool("reg"))).args(["delete", key, "/v", "Igneum Miner", "/f"]).output()
|
|
};
|
|
match out {
|
|
Ok(o) if o.status.success() || !on => Ok(()),
|
|
Ok(o) => Err(String::from_utf8_lossy(&o.stderr).trim().to_string()),
|
|
Err(e) => Err(e.to_string()),
|
|
}
|
|
}
|
|
#[cfg(not(any(target_os = "macos", windows)))]
|
|
{
|
|
let _ = on;
|
|
Err("start at login is not supported on this platform".into())
|
|
}
|
|
}
|
|
|
|
pub fn start_at_login_is_on() -> bool {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
launch_agent_path().exists()
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
quiet(&mut Command::new(tool("reg")))
|
|
.args(["query", r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run", "/v", "Igneum Miner"])
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false)
|
|
}
|
|
#[cfg(not(any(target_os = "macos", windows)))]
|
|
{
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Removes the quarantine flag from the app's own files (macOS): after Gatekeeper lets the app through, each binary
|
|
/// inside would still be checked on its first exec. Best effort; only works on a writable volume.
|
|
pub fn clear_quarantine() {
|
|
#[cfg(target_os = "macos")]
|
|
if let Some(b) = bundle_path() {
|
|
let _ = Command::new(tool("xattr")).args(["-dr", "com.apple.quarantine"]).arg(b.join("Contents")).output();
|
|
}
|
|
}
|
|
|
|
/// The manual instruction for fixing the clock on this platform.
|
|
pub fn clock_hint() -> &'static str {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
"System Settings > General > Date & Time: turn on \"Set time and date automatically\", or run: sudo sntp -sS time.apple.com"
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
"Settings > Time & language > Date & time: turn on \"Set time automatically\" and click \"Sync now\", or run as administrator: w32tm /resync"
|
|
}
|
|
#[cfg(not(any(target_os = "macos", windows)))]
|
|
{
|
|
"run: sudo chronyc makestep, or sudo timedatectl set-ntp true"
|
|
}
|
|
}
|
|
|
|
/// Asks the operating system to set the clock from a time server (an administrator prompt appears). Returns what
|
|
/// happened, for the window. Blocking; call from a thread.
|
|
pub fn sync_clock() -> Result<String, String> {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let out = Command::new(tool("osascript"))
|
|
.args(["-e", "do shell script \"/usr/bin/sntp -sS time.apple.com 2>&1\" with administrator privileges"])
|
|
.output()
|
|
.map_err(|e| e.to_string())?;
|
|
let text = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
|
|
if out.status.success() {
|
|
Ok(if text.trim().is_empty() { "clock set from time.apple.com".into() } else { text.trim().to_string() })
|
|
} else if text.contains("canceled") || text.contains("cancelled") {
|
|
Err("the administrator prompt was cancelled".into())
|
|
} else {
|
|
Err(text.trim().to_string())
|
|
}
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
let cmd = tool("cmd").display().to_string();
|
|
let mut c = elevated_command(&cmd, "/c net start w32time & w32tm /resync /force");
|
|
let out = c.output().map_err(|e| e.to_string())?;
|
|
if out.status.success() {
|
|
Ok("asked Windows Time to resync (w32tm /resync)".into())
|
|
} else {
|
|
Err(String::from_utf8_lossy(&out.stderr).trim().to_string())
|
|
}
|
|
}
|
|
#[cfg(not(any(target_os = "macos", windows)))]
|
|
{
|
|
for args in [vec!["chronyc", "makestep"], vec!["timedatectl", "set-ntp", "true"]] {
|
|
if let Ok(out) = Command::new("pkexec").args(&args).output() {
|
|
if out.status.success() {
|
|
return Ok(format!("ran {}", args.join(" ")));
|
|
}
|
|
}
|
|
}
|
|
Err("neither chronyc nor timedatectl could set the clock".into())
|
|
}
|
|
}
|
|
|
|
/// Runs a command line with administrator rights (one prompt): the NVIDIA power cap needs it on Windows.
|
|
/// Blocking; call from a thread.
|
|
/// The cmd.exe argument for one elevated line. cmd's documented rule: when the text after /c starts with a quote
|
|
/// and holds more than two quotes (two cards, or a cap plus a script), it strips the FIRST and LAST quote and runs
|
|
/// the broken remainder (PC 1, 6 October 2026, 15:45Z: the one approved Power control step ran
|
|
/// `"...\nvidia-smi.exe" -i 0 -pl 460 & "...\nvidia-smi.exe" -i 1 -pl 160 & "...\powershell.exe" ... -File "...ps1"`,
|
|
/// exit 1, no cap applied, no task registered; a single-card PC, two quotes, was fine, which is why PC 2 never
|
|
/// showed it). Wrapping the whole line in one outer pair makes cmd strip exactly those.
|
|
pub fn cmd_c_args(cmdline: &str) -> String {
|
|
format!("/c \"{cmdline}\"")
|
|
}
|
|
|
|
pub fn run_elevated(cmdline: &str) -> Result<(), String> {
|
|
#[cfg(windows)]
|
|
{
|
|
let cmd = tool("cmd").display().to_string();
|
|
let mut c = elevated_command(&cmd, &cmd_c_args(cmdline));
|
|
let out = c.output().map_err(|e| e.to_string())?;
|
|
if out.status.success() {
|
|
Ok(())
|
|
} else {
|
|
let err = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
|
Err(elevated_failure(out.status.code(), &err))
|
|
}
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
let out = Command::new("pkexec").args(["sh", "-c", cmdline]).output().map_err(|e| e.to_string())?;
|
|
if out.status.success() { Ok(()) } else { Err(String::from_utf8_lossy(&out.stderr).trim().to_string()) }
|
|
}
|
|
#[cfg(not(any(windows, target_os = "linux")))]
|
|
{
|
|
let _ = cmdline;
|
|
Err("not supported on this platform".into())
|
|
}
|
|
}
|
|
|
|
/// The reason an elevated step failed, from the launcher's exit code and stderr: exit 251 (the prompt refused,
|
|
/// cancelled or timed out, `elevated_ps_line`) and the "canceled" wording name the prompt; any other code is the
|
|
/// step's own exit (the engine then keeps Power control on: rights were given).
|
|
pub fn elevated_failure(code: Option<i32>, stderr: &str) -> String {
|
|
if code == Some(ELEVATED_LAUNCH_FAILED) || stderr.contains("canceled") || stderr.contains("cancelled") {
|
|
"the administrator prompt was refused, cancelled or timed out".into()
|
|
} else if stderr.is_empty() {
|
|
format!("the elevated step exited with code {}", code.map(|c| c.to_string()).unwrap_or_else(|| "?".into()))
|
|
} else {
|
|
stderr.to_string()
|
|
}
|
|
}
|
|
|
|
/// Doubles the single quotes of `s` for a single-quoted PowerShell literal.
|
|
pub fn ps_quote(s: &str) -> String {
|
|
s.replace('\'', "''")
|
|
}
|
|
|
|
/// The PowerShell line that starts `file args` as administrator (one UAC prompt), waits, and exits with the child's
|
|
/// code. Every elevated launch of the app goes through here (the NVIDIA power cap, the sweep helper, the clock sync,
|
|
/// an elevated remote job) so the console flags live in one place: `-WindowStyle Hidden` is SW_HIDE on the new
|
|
/// process the AppInfo service creates; the elevated child cannot inherit this process's headless console, so without
|
|
/// it the child gets a console of its own (5 October 2026, PC 1 watcher, tools/windows/console-watch*.ps1).
|
|
/// A refused, cancelled or unanswered prompt makes Start-Process throw and `$p` stay null: that is exit 251 with the
|
|
/// reason on stderr, never `exit $p.ExitCode` = 0 (the 5 October 2026 driver job on PC 1 was reported done after
|
|
/// Windows cancelled its prompt at 122 s).
|
|
pub fn elevated_ps_line(file: &str, args: &str) -> String {
|
|
format!(
|
|
"try {{ $p = Start-Process -FilePath '{}' -ArgumentList '{}' -Verb RunAs -Wait -WindowStyle Hidden -PassThru -ErrorAction Stop }} catch {{ Write-Error ('elevated launch failed (UAC refused, cancelled or timed out): ' + $_.Exception.Message); exit 251 }}; if ($null -eq $p) {{ Write-Error 'elevated launch failed: no process'; exit 251 }}; exit $p.ExitCode",
|
|
ps_quote(file),
|
|
ps_quote(args)
|
|
)
|
|
}
|
|
|
|
/// The exit code `elevated_ps_line` uses when the elevated process never started (the prompt refused, cancelled or
|
|
/// timed out).
|
|
pub const ELEVATED_LAUNCH_FAILED: i32 = 251;
|
|
|
|
/// The hidden PowerShell that runs `elevated_ps_line(file, args)`: blocking when run, one UAC prompt on the PC.
|
|
pub fn elevated_command(file: &str, args: &str) -> Command {
|
|
let mut c = Command::new(tool("powershell"));
|
|
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &elevated_ps_line(file, args)]);
|
|
quiet(&mut c);
|
|
c
|
|
}
|
|
|
|
/// Builds a command that runs without a console window on Windows.
|
|
pub fn quiet(cmd: &mut Command) -> &mut Command {
|
|
#[cfg(windows)]
|
|
{
|
|
use std::os::windows::process::CommandExt;
|
|
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
|
|
}
|
|
cmd
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod cmd_tests {
|
|
#[test]
|
|
fn an_elevated_line_is_wrapped_so_cmd_keeps_every_inner_quote() {
|
|
let line = r#""C:\WINDOWS\System32\nvidia-smi.exe" -i 0 -pl 460 & "C:\WINDOWS\System32\nvidia-smi.exe" -i 1 -pl 160 & "C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -File "C:\x\register-power-task.ps1""#;
|
|
let a = super::cmd_c_args(line);
|
|
assert!(a.starts_with("/c \"\"C:\\WINDOWS"), "{a}");
|
|
assert!(a.ends_with("register-power-task.ps1\"\""), "{a}");
|
|
// the inner line is intact between the outer pair
|
|
assert_eq!(&a[4..a.len() - 1], line);
|
|
assert_eq!(super::cmd_c_args("echo hi"), "/c \"echo hi\"");
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod lock_tests {
|
|
#[test]
|
|
fn a_locked_folder_grants_the_user_inheritably_and_covers_what_is_inside() {
|
|
let d = super::icacls_lock_args(true, "Admin");
|
|
assert_eq!(d, vec!["/inheritance:r", "/grant:r", "Admin:(OI)(CI)F"], "inheritable, and never /T (it empties the children)");
|
|
let f = super::icacls_lock_args(false, "Admin");
|
|
assert_eq!(f, vec!["/inheritance:r", "/grant:r", "Admin:F"]);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn elevated_line_is_hidden_and_quoted() {
|
|
let l = super::elevated_ps_line(r"C:\WINDOWS\system32\cmd.exe", "/c echo it's & exit 3");
|
|
assert!(l.starts_with("try { $p = "), "{l}");
|
|
assert!(l.contains("-FilePath 'C:\\WINDOWS\\system32\\cmd.exe' -ArgumentList '/c echo it''s & exit 3' -Verb RunAs -Wait -WindowStyle Hidden -PassThru -ErrorAction Stop } catch {"), "{l}");
|
|
assert!(l.contains("-Verb RunAs"), "{l}");
|
|
assert!(l.contains("-WindowStyle Hidden"), "{l}");
|
|
// a thrown Start-Process (the prompt refused) never falls through to `exit $p.ExitCode`
|
|
assert!(l.contains("exit 251 }; if ($null -eq $p) { Write-Error 'elevated launch failed: no process'; exit 251 }; exit $p.ExitCode"), "{l}");
|
|
assert!(l.ends_with("exit $p.ExitCode"), "{l}");
|
|
assert_eq!(super::ELEVATED_LAUNCH_FAILED, 251);
|
|
assert_eq!(super::elevated_failure(Some(251), "elevated launch failed (UAC refused, cancelled or timed out): ..."), "the administrator prompt was refused, cancelled or timed out");
|
|
assert_eq!(super::elevated_failure(Some(1), "The operation was canceled by the user."), "the administrator prompt was refused, cancelled or timed out");
|
|
assert_eq!(super::elevated_failure(Some(2), ""), "the elevated step exited with code 2");
|
|
assert_eq!(super::elevated_failure(Some(3), "nvidia-smi: bad"), "nvidia-smi: bad");
|
|
assert_eq!(super::ps_quote("a'b''c"), "a''b''''c");
|
|
assert_eq!(super::ps_quote("plain"), "plain");
|
|
}
|
|
|
|
#[test]
|
|
fn elevated_command_is_a_hidden_powershell() {
|
|
let c = super::elevated_command("powershell.exe", "-NoProfile -File \"C:\\x y\\elevated.ps1\"");
|
|
let args: Vec<String> = c.get_args().map(|a| a.to_string_lossy().into_owned()).collect();
|
|
assert_eq!(&args[..4], ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"]);
|
|
assert!(args[4].contains("-ArgumentList '-NoProfile -File \"C:\\x y\\elevated.ps1\"' -Verb RunAs -Wait -WindowStyle Hidden"), "{}", args[4]);
|
|
assert!(c.get_program().to_string_lossy().contains("powershell"));
|
|
}
|
|
|
|
#[test]
|
|
fn token_redaction() {
|
|
let l = "dashboard at http://127.0.0.1:58776/t/a3a01c537130bceeaa1f6118ba48d63e/ (log x)";
|
|
assert_eq!(super::redact(l), "dashboard at http://127.0.0.1:58776/t/<token>/ (log x)");
|
|
assert!(super::carries_token(l));
|
|
assert!(!super::carries_token("GET /t/short/ nothing"));
|
|
assert_eq!(super::redact("no token here"), "no token here");
|
|
}
|
|
}
|