use crate::cli::InstallTakeoverArgs;
#[cfg(windows)]
const MANUAL_INSTALL_URL: &str = "https://reports.qubetx.com/nd300#install";
pub fn run(args: InstallTakeoverArgs) -> i32 {
#[cfg(windows)]
{
match run_windows(args) {
Ok(()) => 0,
Err(error) => {
eprintln!("ND-300 could not complete the fresh-install channel takeover: {error}");
eprintln!("The new installer has not claimed ownership.");
eprintln!(
"Any prior uninstaller that already completed remains completed; review the install help before retrying."
);
eprintln!("Install help: {MANUAL_INSTALL_URL}");
2
}
}
}
#[cfg(not(windows))]
{
let _ = args;
eprintln!("install-takeover is a Windows-installer-only command");
2
}
}
#[cfg(windows)]
fn run_windows(args: InstallTakeoverArgs) -> Result<(), String> {
use crate::cli::InstallTakeoverScope;
let target_machine_scope = target_uses_machine_scope(args.target);
let owners: Vec<_> = super::update::registered_install_owners_for_takeover()?
.into_iter()
.filter(|owner| match args.scope {
InstallTakeoverScope::All => true,
InstallTakeoverScope::User => !owner.machine_scope,
InstallTakeoverScope::Machine => owner.machine_scope,
})
.collect();
if let Some(owner) = owners
.iter()
.find(|owner| owner.machine_scope != target_machine_scope)
{
return Err(format!(
"a registered {} installation uses the opposite Windows install scope; uninstall it first or choose a current installer in the same scope",
owner.origin.json_id()
));
}
let conflicts: Vec<_> = owners
.into_iter()
.filter(|owner| owner_conflicts_with_target(owner, args.target))
.collect();
for owner in conflicts {
uninstall_registered_owner_and_wait(&owner)?;
}
Ok(())
}
#[cfg(windows)]
fn target_uses_machine_scope(target: crate::cli::InstallTakeoverTarget) -> bool {
use crate::cli::InstallTakeoverTarget;
matches!(
target,
InstallTakeoverTarget::MsiGlobal | InstallTakeoverTarget::ExeGlobal
)
}
#[cfg(windows)]
fn owner_conflicts_with_target(
owner: &super::update::RegisteredInstallOwner,
target: crate::cli::InstallTakeoverTarget,
) -> bool {
use super::update::{InstallOrigin, RegisteredUninstall};
use crate::cli::InstallTakeoverTarget;
match target {
InstallTakeoverTarget::MsiGlobal | InstallTakeoverTarget::MsiCorporate => {
matches!(owner.uninstall, RegisteredUninstall::Inno { .. })
}
InstallTakeoverTarget::ExeGlobal => match owner.uninstall {
RegisteredUninstall::Msi { .. } => true,
RegisteredUninstall::Inno { .. } => owner.origin != InstallOrigin::ExeGlobal,
},
InstallTakeoverTarget::ExeCorporate => match owner.uninstall {
RegisteredUninstall::Msi { .. } => true,
RegisteredUninstall::Inno { .. } => owner.origin != InstallOrigin::ExeCorporate,
},
InstallTakeoverTarget::Standalone => true,
}
}
#[cfg(windows)]
fn uninstall_registered_owner_and_wait(
owner: &super::update::RegisteredInstallOwner,
) -> Result<(), String> {
use super::update::RegisteredUninstall;
use std::process::Command;
use std::time::{Duration, Instant};
let status = match &owner.uninstall {
RegisteredUninstall::Msi { product_code } => {
Command::new(super::uninstall::system_msiexec_path()?)
.args(["/x", product_code, "/qn", "/norestart"])
.status()
.map_err(|error| format!("could not start MSI uninstaller: {error}"))?
}
RegisteredUninstall::Inno { executable } => Command::new(executable)
.args(["/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART", "/SP-"])
.status()
.map_err(|error| {
format!(
"could not start validated Inno uninstaller {}: {error}",
executable.display()
)
})?,
};
let code = status.code().unwrap_or(-1);
let accepted = status.success()
|| matches!(
owner.uninstall,
RegisteredUninstall::Msi { .. }
if matches!(code, 1605 | 3010)
);
if !accepted {
return Err(format!(
"registered {} uninstaller exited {code}",
owner.origin.json_id()
));
}
let deadline = Instant::now() + Duration::from_secs(30);
while Instant::now() < deadline {
if !super::update::registered_install_owners_for_takeover()?.contains(owner) {
return Ok(());
}
std::thread::sleep(Duration::from_millis(250));
}
Err(format!(
"registered {} owner remained after its uninstaller exited",
owner.origin.json_id()
))
}
#[cfg(all(test, windows))]
mod tests {
use super::*;
use crate::actions::update::{InstallOrigin, RegisteredInstallOwner, RegisteredUninstall};
use crate::cli::InstallTakeoverTarget;
use std::path::PathBuf;
fn owner(origin: InstallOrigin, uninstall: RegisteredUninstall) -> RegisteredInstallOwner {
RegisteredInstallOwner {
origin,
install_location: PathBuf::from(r"C:\fixture"),
uninstall,
machine_scope: matches!(origin, InstallOrigin::MsiGlobal | InstallOrigin::ExeGlobal),
}
}
#[test]
fn update_target_keeps_same_inno_channel_but_same_scope_msi_takes_over() {
let global_exe = owner(
InstallOrigin::ExeGlobal,
RegisteredUninstall::Inno {
executable: PathBuf::from(r"C:\fixture\unins000.exe"),
},
);
assert!(!owner_conflicts_with_target(
&global_exe,
InstallTakeoverTarget::ExeGlobal
));
assert!(owner_conflicts_with_target(
&global_exe,
InstallTakeoverTarget::MsiGlobal
));
}
#[test]
fn msi_targets_never_launch_a_nested_msi_uninstall() {
let global_msi = owner(
InstallOrigin::MsiGlobal,
RegisteredUninstall::Msi {
product_code: "{A1111111-B222-4CCC-8DDD-E55555555555}".to_string(),
},
);
assert!(!owner_conflicts_with_target(
&global_msi,
InstallTakeoverTarget::MsiGlobal
));
assert!(owner_conflicts_with_target(
&global_msi,
InstallTakeoverTarget::ExeGlobal
));
}
#[test]
fn machine_and_user_installer_scopes_are_never_crossed() {
assert!(target_uses_machine_scope(InstallTakeoverTarget::MsiGlobal));
assert!(target_uses_machine_scope(InstallTakeoverTarget::ExeGlobal));
assert!(!target_uses_machine_scope(
InstallTakeoverTarget::MsiCorporate
));
assert!(!target_uses_machine_scope(
InstallTakeoverTarget::ExeCorporate
));
assert!(!target_uses_machine_scope(
InstallTakeoverTarget::Standalone
));
let managed_corporate = owner(
InstallOrigin::MsiCorporate,
RegisteredUninstall::Msi {
product_code: "{A1111111-B222-4CCC-8DDD-E55555555555}".to_string(),
},
);
assert!(!managed_corporate.machine_scope);
}
}