295 lines
9.7 KiB
Rust
295 lines
9.7 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;
|
|
|
|
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);
|
|
}
|
|
#[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("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 _ = dir;
|
|
let user = std::env::var("USERNAME").unwrap_or_default();
|
|
if !user.is_empty() {
|
|
let _ = Command::new("icacls")
|
|
.arg(path)
|
|
.args(["/inheritance:r", "/grant:r", &format!("{user}:F")])
|
|
.output();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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("open").arg(url).spawn();
|
|
#[cfg(windows)]
|
|
let _ = Command::new("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("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(" ");
|
|
Command::new("reg").args(["add", key, "/v", "Igneum Miner", "/t", "REG_SZ", "/d", &cmd, "/f"]).output()
|
|
} else {
|
|
Command::new("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)]
|
|
{
|
|
Command::new("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("xattr").args(["-dr", "com.apple.quarantine"]).arg(b.join("Contents")).output();
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
}
|