mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
craft-update: allow opted-in quiet per-user MSI self-updates
* Add opt-in quiet per-user MSI self-updates --------- Co-authored-by: songzhihui <song19891121@qq.com>
This commit is contained in:
1 parent
cb9d29c913
commit
a44920f685
3 files changed
+100
-21
No files matched your search
@@ -2201,19 +2201,30 @@ mod freebsd {
|
||||
/// without starting an installer.
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
fn msi_command(program: &Path, msi: &Path) -> Result<std::process::Command, String> {
|
||||
msi_command_ui(program, msi, false)
|
||||
}
|
||||
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
fn msi_command_ui(program: &Path, msi: &Path, quiet: bool) -> Result<std::process::Command, String> {
|
||||
let msi = msi_path(msi)?;
|
||||
let mut command = process::command(program);
|
||||
// A verbose log beside the package: Windows Installer's own errors (1603 above all) say
|
||||
// nothing without it.
|
||||
command.arg("/i").arg(&msi).args(["/passive", "/norestart", "/l*v"]).arg(msi_log(&msi));
|
||||
command.arg("/i").arg(&msi).args([if quiet { "/qn" } else { "/passive" }, "/norestart", "/l*v"]).arg(msi_log(&msi));
|
||||
Ok(command)
|
||||
}
|
||||
|
||||
/// The self-update command: [`msi_command`] plus the properties that keep the installed copy's
|
||||
/// Windows Installer context (`msi_copy::update_arguments`).
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
#[cfg(test)]
|
||||
fn reinstall_command(program: &Path, msi: &Path, context: &[&str]) -> Result<std::process::Command, String> {
|
||||
let mut command = msi_command(program, msi)?;
|
||||
reinstall_command_ui(program, msi, context, false)
|
||||
}
|
||||
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
fn reinstall_command_ui(program: &Path, msi: &Path, context: &[&str], quiet: bool) -> Result<std::process::Command, String> {
|
||||
// A per-machine /qn install cannot request elevation. Keep its existing visible route.
|
||||
let mut command = msi_command_ui(program, msi, quiet && !context.contains(&"ALLUSERS=1"))?;
|
||||
command.args(context);
|
||||
Ok(command)
|
||||
}
|
||||
@@ -2240,9 +2251,7 @@ pub(crate) fn msi_path(msi: &Path) -> Result<PathBuf, String> {
|
||||
pub mod windows {
|
||||
use crate::protocol::CANCELLED;
|
||||
|
||||
use super::{
|
||||
App, INSTALLER, JobPhase, Path, PathBuf, Stop, cancelled, msi_command, msi_folder_argument, msi_log, process, reinstall_command, turn,
|
||||
};
|
||||
use super::{App, INSTALLER, JobPhase, Path, PathBuf, Stop, cancelled, msi_command, msi_folder_argument, msi_log, process, turn};
|
||||
|
||||
fn msiexec() -> PathBuf {
|
||||
std::env::var_os("SystemRoot").map_or_else(|| PathBuf::from(r"C:\Windows"), PathBuf::from).join(r"System32\msiexec.exe")
|
||||
@@ -2280,8 +2289,12 @@ pub mod windows {
|
||||
/// context; `folder`, when given, is where it goes. A failure names the installer's log,
|
||||
/// which is the only place 1603 is explained.
|
||||
pub fn reinstall(msi: &Path, context: &[&str], folder: Option<&Path>) -> Result<(), String> {
|
||||
reinstall_with_ui(msi, context, folder, false)
|
||||
}
|
||||
|
||||
pub(crate) fn reinstall_with_ui(msi: &Path, context: &[&str], folder: Option<&Path>, quiet: bool) -> Result<(), String> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
let mut command = reinstall_command(&msiexec(), msi, context)?;
|
||||
let mut command = super::reinstall_command_ui(&msiexec(), msi, context, quiet)?;
|
||||
if let Some(dir) = folder {
|
||||
command.raw_arg(msi_folder_argument(dir)?);
|
||||
}
|
||||
@@ -2334,6 +2347,27 @@ pub mod windows {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn quiet_self_update_keeps_logs_context_and_required_permission_ui() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("launcher update.msi");
|
||||
std::fs::write(&file, b"test package").unwrap();
|
||||
for (context, quiet, expected) in [
|
||||
(&[][..], false, "/passive"),
|
||||
(&[][..], true, "/qn"),
|
||||
(&["ALLUSERS=2", "MSIINSTALLPERUSER=1"][..], true, "/qn"),
|
||||
(&["ALLUSERS=1"][..], true, "/passive"),
|
||||
] {
|
||||
let cmd = reinstall_command_ui(Path::new("msiexec.exe"), &file, context, quiet).unwrap();
|
||||
let args: Vec<_> = cmd.get_args().map(|a| a.to_string_lossy().into_owned()).collect();
|
||||
assert_eq!(args[2], expected);
|
||||
assert_eq!(&args[3..5], ["/norestart", "/l*v"]);
|
||||
assert!(args[5].ends_with("launcher update.log"));
|
||||
assert_eq!(&args[6..], context);
|
||||
assert_eq!(args.iter().filter(|a| a.as_str() == "/qn" || a.as_str() == "/passive").count(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn a_system_flatpak_bundle_is_reinstalled_with_pkexec_on_that_installation() {
|
||||
|
||||
@@ -143,6 +143,14 @@ pub(crate) mod windows {
|
||||
Ok(plan(resolved, &dir, app.name, &profile, env_path("ProgramFiles").as_deref()))
|
||||
}
|
||||
|
||||
/// Quiet is limited to in-place per-user updates; migrations retain their permission UI.
|
||||
pub(crate) fn run_with_ui(msi: &Path, step: &Step, quiet: bool) -> Result<(), String> {
|
||||
if let Step::Update(context) = step {
|
||||
return crate::install::windows::reinstall_with_ui(msi, context, None, quiet);
|
||||
}
|
||||
run(msi, step)
|
||||
}
|
||||
|
||||
/// Runs `step` with the downloaded package `msi`.
|
||||
pub(crate) fn run(msi: &Path, step: &Step) -> Result<(), String> {
|
||||
match step {
|
||||
|
||||
@@ -768,8 +768,16 @@ impl std::fmt::Debug for Restart {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`update`] without starting the helper: the caller starts the returned [`Restart`] when the
|
||||
/// app's exit is certain. On macOS and Linux the update is in place when this returns.
|
||||
/// Options for an explicitly opted-in background update. Other platforms keep their existing
|
||||
/// installation behaviour; permission prompts are never suppressed.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct UpdateOptions {
|
||||
/// Hide progress for a per-user Windows MSI. Machine installs retain their permission UI.
|
||||
pub quiet_msi: bool,
|
||||
}
|
||||
|
||||
/// [`update`] without starting the helper: start the returned [`Restart`] when exit is certain.
|
||||
/// On macOS and Linux the update is already in place when this returns.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn prepare_update(
|
||||
app: &App,
|
||||
@@ -783,6 +791,27 @@ pub fn prepare_update(
|
||||
last: &dyn Fn() -> Result<(), String>,
|
||||
running_version: Option<&str>,
|
||||
) -> Result<Restart, String> {
|
||||
prepare_update_with_options(app, install, release, platform, http, paths, stop, phase, last, running_version, UpdateOptions::default())
|
||||
}
|
||||
|
||||
/// Like [`prepare_update`], with an explicit choice of Windows Installer progress UI.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn prepare_update_with_options(
|
||||
app: &App,
|
||||
install: &Install,
|
||||
release: &Release,
|
||||
platform: Platform,
|
||||
http: &Http,
|
||||
paths: &Paths,
|
||||
stop: &Stop,
|
||||
phase: &mut dyn FnMut(JobPhase),
|
||||
last: &dyn Fn() -> Result<(), String>,
|
||||
running_version: Option<&str>,
|
||||
options: UpdateOptions,
|
||||
) -> Result<Restart, String> {
|
||||
// The option only changes Windows MSI helpers.
|
||||
#[cfg(not(windows))]
|
||||
let _ = options;
|
||||
let cur_v = running_version.and_then(|v| current_version(app, v));
|
||||
if let (Some(rel_v), Some(cur_v)) = (release.version.as_ref(), cur_v.as_ref())
|
||||
&& !version::offers_update(rel_v, cur_v)
|
||||
@@ -855,7 +884,7 @@ pub fn prepare_update(
|
||||
relaunch_flatpak_after_exit(app)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
Install::Msi(exe) => windows_helper(app, paths, release, pid, exe, &msi_finish(&file)?),
|
||||
Install::Msi(exe) => windows_helper(app, paths, release, pid, exe, &msi_finish(&file, options.quiet_msi)?),
|
||||
#[cfg(windows)]
|
||||
Install::Portable(exe) => {
|
||||
exe.parent().ok_or_else(|| format!("{} isn't in a folder", app.name))?;
|
||||
@@ -1354,6 +1383,8 @@ pub const FINISH_FLAG: &str = "--finish-update";
|
||||
enum Finish {
|
||||
/// Run the new MSI, which reinstalls into the same folder.
|
||||
Msi(PathBuf),
|
||||
/// Per-user MSI with no installer progress window.
|
||||
QuietMsi(PathBuf),
|
||||
/// Unpack the new portable zip into `tmp` and copy it over the launcher's folder.
|
||||
Portable { zip: PathBuf, tmp: PathBuf },
|
||||
}
|
||||
@@ -1365,6 +1396,7 @@ fn helper_args(pid: u32, exe: &Path, finish: &Finish) -> Vec<OsString> {
|
||||
let mut args: Vec<OsString> = vec![FINISH_FLAG.into(), pid.to_string().into(), exe.into()];
|
||||
match finish {
|
||||
Finish::Msi(msi) => args.extend(["msi".into(), msi.into()]),
|
||||
Finish::QuietMsi(msi) => args.extend(["msi-quiet".into(), msi.into()]),
|
||||
Finish::Portable { zip, tmp } => args.extend(["portable".into(), zip.into(), tmp.into()]),
|
||||
}
|
||||
args
|
||||
@@ -1380,6 +1412,7 @@ fn parse_helper_args(args: &[OsString]) -> Option<(u32, PathBuf, Finish)> {
|
||||
let pid = pid.to_str()?.parse().ok()?;
|
||||
let finish = match step {
|
||||
[kind, msi] if kind == "msi" => Finish::Msi(msi.into()),
|
||||
[kind, msi] if kind == "msi-quiet" => Finish::QuietMsi(msi.into()),
|
||||
[kind, zip, tmp] if kind == "portable" => Finish::Portable { zip: zip.into(), tmp: tmp.into() },
|
||||
_ => return None,
|
||||
};
|
||||
@@ -1387,8 +1420,8 @@ fn parse_helper_args(args: &[OsString]) -> Option<(u32, PathBuf, Finish)> {
|
||||
}
|
||||
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
fn msi_finish(file: &Path) -> Result<Finish, String> {
|
||||
install::msi_path(file).map(Finish::Msi)
|
||||
fn msi_finish(file: &Path, quiet: bool) -> Result<Finish, String> {
|
||||
install::msi_path(file).map(|file| if quiet { Finish::QuietMsi(file) } else { Finish::Msi(file) })
|
||||
}
|
||||
|
||||
/// The cache folder the update helper is copied into.
|
||||
@@ -1442,8 +1475,8 @@ pub fn finish(app: &App, args: &[OsString]) -> Option<Result<(), String>> {
|
||||
return Some(Err(e));
|
||||
}
|
||||
// Read before the step: the update may move or replace the old exe.
|
||||
let registered =
|
||||
matches!(step, Finish::Msi(_)) && matches!(crate::msi_copy::windows::find(app, &exe), Some(crate::msi_copy::MsiCopy::Registered { .. }));
|
||||
let registered = matches!(step, Finish::Msi(_) | Finish::QuietMsi(_))
|
||||
&& matches!(crate::msi_copy::windows::find(app, &exe), Some(crate::msi_copy::MsiCopy::Registered { .. }));
|
||||
let wait = |pid| process::wait_for_exit(pid, std::time::Duration::from_secs(60));
|
||||
let start = |exe: &Path| {
|
||||
signed_like(app, &me, exe)
|
||||
@@ -1667,7 +1700,7 @@ fn check_helper(app: &App, me: &Path, paths: &Paths, exe: &Path, step: &Finish)
|
||||
let downloads = canonical(&paths.downloads())?;
|
||||
let in_downloads = |file: &Path| canonical(file).map(|f| f.parent() == Some(downloads.as_path()));
|
||||
let allowed = match (detect(app, &exe), step) {
|
||||
(Some(Install::Msi(installed)), Finish::Msi(msi)) => installed == exe && in_downloads(msi)?,
|
||||
(Some(Install::Msi(installed)), Finish::Msi(msi) | Finish::QuietMsi(msi)) => installed == exe && in_downloads(msi)?,
|
||||
(Some(Install::Portable(installed) | Install::Standalone(installed)), Finish::Portable { zip, tmp }) => {
|
||||
installed == exe
|
||||
&& in_downloads(zip)?
|
||||
@@ -1701,8 +1734,11 @@ fn step_after_wait(app: &App, reference: &Path, pid: u32, exe: &Path, step: &Fin
|
||||
let waited = wait(pid);
|
||||
let result = if waited {
|
||||
match step {
|
||||
Finish::Msi(msi) => {
|
||||
signed_like(app, reference, msi).and_then(|()| msi_step(app, exe)).and_then(|step| crate::msi_migrate::windows::run(msi, &step))
|
||||
Finish::Msi(msi) | Finish::QuietMsi(msi) => {
|
||||
let quiet = matches!(step, Finish::QuietMsi(_));
|
||||
signed_like(app, reference, msi)
|
||||
.and_then(|()| msi_step(app, exe))
|
||||
.and_then(|step| crate::msi_migrate::windows::run_with_ui(msi, &step, quiet))
|
||||
}
|
||||
Finish::Portable { zip, tmp } => match (exe.parent(), exe.file_name()) {
|
||||
(Some(dir), Some(executable)) => crate::portable::unpack_over(zip, tmp, dir, executable, &|file| {
|
||||
@@ -1716,7 +1752,7 @@ fn step_after_wait(app: &App, reference: &Path, pid: u32, exe: &Path, step: &Fin
|
||||
Err(format!("{} was still running after a minute, so the update wasn't installed", app.name))
|
||||
};
|
||||
match step {
|
||||
Finish::Msi(msi) => {
|
||||
Finish::Msi(msi) | Finish::QuietMsi(msi) => {
|
||||
let _ = std::fs::remove_file(msi);
|
||||
}
|
||||
// `unpack_over` removes the staging folder, unless it holds files it couldn't put back.
|
||||
@@ -2525,13 +2561,13 @@ mod tests {
|
||||
std::fs::write(&file, b"test package").unwrap();
|
||||
let link = dir.path().join("cached.msi");
|
||||
std::os::unix::fs::symlink(&file, &link).unwrap();
|
||||
assert_eq!(msi_finish(&link).unwrap(), Finish::Msi(file.canonicalize().unwrap()));
|
||||
assert_eq!(msi_finish(&link, false).unwrap(), Finish::Msi(file.canonicalize().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_missing_msi_cannot_schedule_a_self_update() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
assert!(msi_finish(&dir.path().join("missing.msi")).is_err());
|
||||
assert!(msi_finish(&dir.path().join("missing.msi"), false).is_err());
|
||||
}
|
||||
|
||||
/// The helper reads back exactly what the launcher passed it, paths with spaces and
|
||||
@@ -2542,7 +2578,8 @@ mod tests {
|
||||
let msi = Finish::Msi(PathBuf::from(r"C:\Users\O'Brien\AppData\Local\ArtCraft Launcher\cache\artcraft-launcher-0.7.0-windows-x64.msi"));
|
||||
let portable =
|
||||
Finish::Portable { zip: PathBuf::from(r"D:\Tools\x 1.zip"), tmp: PathBuf::from(r"D:\Tools\ArtCraftLauncherData\cache\launcher-update") };
|
||||
for finish in [msi, portable] {
|
||||
let quiet = Finish::QuietMsi(PathBuf::from(r"C:\Updates\a b.msi"));
|
||||
for finish in [msi, quiet, portable] {
|
||||
let args = helper_args(4242, exe, &finish);
|
||||
assert_eq!(args.first().map(OsString::as_os_str), Some(std::ffi::OsStr::new(FINISH_FLAG)));
|
||||
assert_eq!(parse_helper_args(&args), Some((4242, exe.to_path_buf(), finish)));
|
||||
|
||||
Reference in new issue
Block a user