master
John Lauer 2.0.413: missing verbs + behaviors (assisted_click, import, fg-watch, url lint, notices, chrome-stable, eval frame, mov… 0dd55ee 2mo ago
//! Real Google Chrome install (Node: chrome.js installChromeStable / chromeStableState). pup drives
//! the machine's INSTALLED Chrome or Edge by default; `pup_use {browser:"chrome", install:true}`
//! fetches and installs actual Google Chrome. Windows: the silent installer, escalating to an
//! elevated (UAC) install; macOS: the .dmg copied to /Applications. The phase drives pup_readiness's
//! chromeStable* fields. NO ab notify re-arm loop here (the Node build's peer-ping UX stays in Node
//! until the exe has the direct-API notify channel); we run one elevated prompt and report the phase.

use crate::{detect_chrome, Host};
use serde_json::{json, Value};
use std::sync::Mutex;
use std::time::{Duration, Instant};

#[derive(Clone)]
pub struct StableState { pub phase: String, pub error: Option<String>, pub error_code: Option<String>, pub in_flight: bool }
static STATE: Mutex<Option<StableState>> = Mutex::new(None);

pub fn snapshot() -> Value {
    let g = STATE.lock().unwrap();
    let st = g.clone().unwrap_or(StableState { phase: "idle".into(), error: None, error_code: None, in_flight: false });
    json!({ "installingChrome": st.in_flight, "chromeStablePhase": st.phase, "chromeStableAwaitingApproval": st.phase == "awaiting_uac",
        "chromeStableError": st.error, "chromeStableNotifyChannel": Value::Null, "chromeStableNotifyPeers": Value::Null })
}
fn set(phase: &str, code: Option<&str>, err: Option<&str>, in_flight: bool) {
    *STATE.lock().unwrap() = Some(StableState { phase: phase.into(), error_code: code.map(|s| s.into()), error: err.map(|s| s.into()), in_flight });
}

/// Kick off the install in the background (idempotent). Poll `snapshot()` / pup_readiness for phase.
pub fn install_background(host: std::sync::Arc<dyn Host>, elevate: Option<bool>) {
    { let g = STATE.lock().unwrap(); if g.as_ref().map(|s| s.in_flight).unwrap_or(false) { return; } }
    set("installing", None, None, true);
    std::thread::Builder::new().name("chrome-stable".into()).spawn(move || { let _ = install_blocking(&*host, elevate); }).ok();
}

pub fn install_blocking(host: &dyn Host, elevate: Option<bool>) -> Value {
    set("installing", None, None, true);
    if let Some(c) = detect_chrome(host) { set("ready", None, None, false); return json!({ "ok": true, "executablePath": c.executable_path.to_string_lossy(), "alreadyInstalled": true }); }
    let res = if cfg!(target_os = "macos") { install_mac(host) } else if cfg!(windows) { install_win(host, elevate) } else {
        set("failed", Some("chrome_stable_unsupported_os"), Some("Auto-installing Google Chrome is only wired up for Windows and macOS."), false);
        json!({ "ok": false, "errorCode": "chrome_stable_unsupported_os" })
    };
    res
}

fn install_mac(host: &dyn Host) -> Value {
    let dmg = host.temp_dir().join(format!("chrome_installer_{}.dmg", std::process::id()));
    if let Err(e) = host.download_file("https://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg", &dmg) { set("failed", Some("chrome_download_failed"), Some(&e), false); return json!({ "ok": false, "errorCode": "chrome_download_failed", "error": e }); }
    // hdiutil attach, ditto Google Chrome.app to /Applications, detach. Node: the darwin branch.
    let script = format!("set -e; MP=$(hdiutil attach -nobrowse -readonly '{}' | grep Volumes | sed 's/.*\\/Volumes/\\/Volumes/'); ditto \"$MP/Google Chrome.app\" \"/Applications/Google Chrome.app\" || ditto \"$MP/Google Chrome.app\" \"$HOME/Applications/Google Chrome.app\"; hdiutil detach \"$MP\" -quiet; rm -f '{}'", dmg.display(), dmg.display());
    let _ = host.run_shell_script(&script, Duration::from_secs(180));
    match detect_chrome(host) { Some(c) => { set("ready", None, None, false); json!({ "ok": true, "executablePath": c.executable_path.to_string_lossy() }) } None => { set("failed", Some("chrome_stable_not_detected"), Some("Copied Google Chrome.app but it did not detect afterward."), false); json!({ "ok": false, "errorCode": "chrome_stable_not_detected" }) } }
}

fn install_win(host: &dyn Host, elevate: Option<bool>) -> Value {
    let tmp = host.temp_dir().join(format!("chrome_installer_{}.exe", std::process::id()));
    if let Err(e) = host.download_file("https://dl.google.com/chrome/install/standalonesetup64.exe", &tmp) { set("failed", Some("chrome_download_failed"), Some(&e), false); return json!({ "ok": false, "errorCode": "chrome_download_failed", "error": e }); }
    let tmp_s = tmp.to_string_lossy().replace('\'', "''");
    // silent first (no prompt on a normal PC), then poll ~45s
    if elevate != Some(true) {
        let _ = host.run_shell_script(&format!("Start-Process -FilePath '{tmp_s}' -ArgumentList '/silent','/install' -WindowStyle Hidden"), Duration::from_secs(15));
        let t0 = Instant::now(); while t0.elapsed() < Duration::from_secs(45) { if let Some(c) = detect_chrome(host) { let _ = std::fs::remove_file(&tmp); set("ready", None, None, false); return json!({ "ok": true, "executablePath": c.executable_path.to_string_lossy() }); } std::thread::sleep(Duration::from_millis(1500)); }
    }
    // escalate: one elevated install with a UAC prompt (Start-Process -Verb RunAs)
    if elevate != Some(false) {
        set("awaiting_uac", None, None, true);
        let _ = host.run_shell_script(&format!("try {{ Start-Process -FilePath '{tmp_s}' -ArgumentList '/silent','/install' -Verb RunAs -WindowStyle Hidden -ErrorAction Stop }} catch {{ exit 3 }}"), Duration::from_secs(20));
        let t0 = Instant::now(); while t0.elapsed() < Duration::from_secs(150) { if let Some(c) = detect_chrome(host) { let _ = std::fs::remove_file(&tmp); set("ready", None, None, false); return json!({ "ok": true, "executablePath": c.executable_path.to_string_lossy() }); } std::thread::sleep(Duration::from_millis(2500)); }
    }
    let _ = std::fs::remove_file(&tmp);
    let (code, msg) = if elevate == Some(false) { ("chrome_stable_not_detected", "Ran the Chrome installer but chrome.exe did not appear (elevation was declined via elevate:false). pup still works with Edge in the meantime.") } else { ("chrome_stable_needs_approval", "Chrome needs a Windows approval (UAC) that was not accepted in time. Re-run pup_use {browser:\"chrome\", install:true} and click YES on the prompt; pup drives Edge until then.") };
    set("failed", Some(code), Some(msg), false);
    json!({ "ok": false, "errorCode": code, "error": msg })
}