Skip to content

Commit 8c87634

Browse files
fix: quick-win bug fixes for multiple issues
- topgrade-rs#869: Replace waydroid panic with graceful SkipStep error - topgrade-rs#491: Remove deny_unknown_fields from ConfigFile to tolerate unknown keys - topgrade-rs#1016: Trim whitespace from remote_topgrades entries - topgrade-rs#1210: DragonFly audit only treats exit 1 as non-error (not all failures) - topgrade-rs#595: Reset terminal title after remote SSH execution - topgrade-rs#577: Check /var/run/reboot-required after apt upgrade and notify - topgrade-rs#861: Prefer systemctl reboot on systemd-based systems - topgrade-rs#1647: Set NO_COLOR=1 for flatpak update commands - topgrade-rs#1228: Improve UnsupportedSudo error with actionable guidance - topgrade-rs#876: Forward --disable flags to WSL topgrade invocation - topgrade-rs#987: Add Homebrew paths for antidote detection - topgrade-rs#993: Check for needrestart pacman hook file before running needrestart - topgrade-rs#1239: Fall back to restarting clamav-freshclam.service if freshclam fails Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1a10b1a commit 8c87634

11 files changed

Lines changed: 126 additions & 29 deletions

File tree

locales/app.yml

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,14 +1258,14 @@ _version: 2
12581258
zh_CN: "检测到 Windows Sudo,但其正在使用“新窗口”模式"
12591259
zh_TW: "偵測到 Windows Sudo,但它正在使用「新視窗」模式"
12601260
de: "Windows Sudo gefunden, aber es verwendet den Modus „In einem neuen Fenster“"
1261-
"{sudo_kind} does not support the {option} option":
1262-
en: "%{sudo_kind} does not support the %{option} option"
1263-
lt: "%{sudo_kind} nepalaiko parinkties %{option}"
1264-
es: "%{sudo_kind} no admite la opción %{option}"
1265-
fr: "%{sudo_kind} ne prend pas en charge l’option %{option}"
1266-
zh_CN: "%{sudo_kind} 不支持 %{option} 选项"
1267-
zh_TW: "%{sudo_kind} 不支援 %{option} 選項"
1268-
de: "%{sudo_kind} unterstützt die Option %{option} nicht"
1261+
"{sudo_kind} does not support the {option} option. Consider using a different sudo provider (e.g., sudo, doas, gsudo) that supports this feature.":
1262+
en: "%{sudo_kind} does not support the %{option} option. Consider using a different sudo provider (e.g., sudo, doas, gsudo) that supports this feature."
1263+
lt: "%{sudo_kind} nepalaiko parinkties %{option}’. Apsvarstykite galimybę naudoti kitą sudo teikėją (pvz., sudo, doas, gsudo), kuris palaiko šią funkciją."
1264+
es: "%{sudo_kind} no admite la opción %{option}’. Considere usar un proveedor sudo diferente (p. ej., sudo, doas, gsudo) que admita esta función."
1265+
fr: "%{sudo_kind} ne prend pas en charge l’option %{option}’. Envisagez d’utiliser un autre fournisseur sudo (par exemple, sudo, doas, gsudo) qui prend en charge cette fonctionnalité."
1266+
zh_CN: "%{sudo_kind} 不支持 %{option} 选项。请考虑使用支持此功能的其他 sudo 提供程序(例如 sudo、doas、gsudo)。"
1267+
zh_TW: "%{sudo_kind} 不支援 %{option} 選項。請考慮使用支援此功能的其他 sudo 提供者(例如 sudo、doas、gsudo)。"
1268+
de: "%{sudo_kind} unterstützt die Option %{option} nicht. Erwägen Sie die Verwendung eines anderen sudo-Anbieters (z.B. sudo, doas, gsudo), der diese Funktion unterstützt."
12691269
"sudo as user '{user}'":
12701270
en: "sudo as user '%{user}'"
12711271
lt: "sudo kaip vartotojas '%{user}'"
@@ -1834,3 +1834,25 @@ _version: 2
18341834
zh_CN: "现在安装所选驱动程序吗?将首先创建一个还原点。(y/N)"
18351835
zh_TW: "現在要安裝所選的驅動程式嗎?將先建立還原點。(y/N)"
18361836
de: "Ausgewählte Treiber jetzt installieren? Zuerst wird ein Wiederherstellungspunkt erstellt. (y/N)"
1837+
"Could not parse waydroid status output (no Session line found)":
1838+
en: "Could not parse waydroid status output (no Session line found)"
1839+
"A reboot is required to complete the system update.":
1840+
en: "A reboot is required to complete the system update."
1841+
"Restarted clamav-freshclam.service to trigger database update":
1842+
en: "Restarted clamav-freshclam.service to trigger database update"
1843+
"Micromamba":
1844+
en: "Micromamba"
1845+
"Microsoft AutoUpdate":
1846+
en: "Microsoft AutoUpdate"
1847+
"(P)oweroff":
1848+
en: "(P)oweroff"
1849+
"Powering off...":
1850+
en: "Powering off..."
1851+
"Micromamba not found":
1852+
en: "Micromamba not found"
1853+
"msupdate not found":
1854+
en: "msupdate not found"
1855+
"MSYS2":
1856+
en: "MSYS2"
1857+
"Available steps:":
1858+
en: "Available steps:"

src/config.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,6 @@ pub struct StepOrder {
631631
}
632632

633633
#[derive(Deserialize, Default, Debug, Merge)]
634-
#[serde(deny_unknown_fields)]
635634
/// Configuration file
636635
pub struct ConfigFile {
637636
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
@@ -1383,12 +1382,18 @@ impl Config {
13831382
self.opt.env_variables()
13841383
}
13851384

1386-
/// List of remote hosts to run Topgrade in
1387-
pub fn remote_topgrades(&self) -> Option<&Vec<String>> {
1388-
self.config_file
1389-
.misc
1390-
.as_ref()
1391-
.and_then(|misc| misc.remote_topgrades.as_ref())
1385+
/// List of remote hosts to run Topgrade in.
1386+
/// Whitespace is trimmed from each entry so that `remote_topgrades = ["host1 ", " host2"]`
1387+
/// works correctly.
1388+
pub fn remote_topgrades(&self) -> Option<Vec<String>> {
1389+
self.config_file.misc.as_ref().and_then(|misc| {
1390+
misc.remote_topgrades.as_ref().map(|v| {
1391+
v.iter()
1392+
.map(|s| s.trim().to_string())
1393+
.filter(|s| !s.is_empty())
1394+
.collect()
1395+
})
1396+
})
13921397
}
13931398

13941399
/// Path to Topgrade executable used for all remote hosts
@@ -1919,6 +1924,11 @@ impl Config {
19191924
self.opt.verbose
19201925
}
19211926

1927+
/// Return the list of step names passed via `--disable` on the CLI.
1928+
pub fn cli_disabled_steps(&self) -> &[Step] {
1929+
&self.opt.disable
1930+
}
1931+
19221932
/// After loading the config file, filter directives consist of 3 parts:
19231933
///
19241934
/// 1. directives from the configuration file

src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ impl Display for UnsupportedSudo<'_> {
8282
f,
8383
"{}",
8484
t!(
85-
"{sudo_kind} does not support the {option} option",
85+
"{sudo_kind} does not support the '{option}' option. Consider using a different sudo provider (e.g., sudo, doas, gsudo) that supports this feature.",
8686
sudo_kind = self.sudo_kind,
8787
option = self.option
8888
)

src/step.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -653,9 +653,8 @@ impl Step {
653653
.iter()
654654
.filter(|t| ctx.config().should_execute_remote(hostname(), t))
655655
{
656-
runner.execute(*self, format!("Remote ({remote_topgrade})"), || {
657-
crate::ssh::ssh_step(ctx, remote_topgrade)
658-
})?;
656+
let host = remote_topgrade.clone();
657+
runner.execute(*self, format!("Remote ({host})"), || crate::ssh::ssh_step(ctx, &host))?;
659658
}
660659
}
661660
}

src/steps/generic.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,6 +1608,22 @@ pub fn run_freshclam(ctx: &ExecutionContext) -> Result<()> {
16081608
match sudo.execute(ctx, freshclam)?.status_checked() {
16091609
Ok(()) => Ok(()), // Success! The output of only the sudo'ed process is written.
16101610
Err(err) => {
1611+
// If freshclam fails, try restarting the clamav-freshclam systemd service as a fallback.
1612+
// This handles cases where freshclam is managed by systemd and fails due to lock conflicts.
1613+
#[cfg(target_os = "linux")]
1614+
if which("systemctl").is_some() {
1615+
debug!("freshclam failed, attempting to restart clamav-freshclam.service");
1616+
if let Ok(()) = sudo
1617+
.execute(ctx, "systemctl")
1618+
.and_then(|mut cmd| cmd.args(["restart", "clamav-freshclam.service"]).status_checked())
1619+
{
1620+
println!(
1621+
"{}",
1622+
t!("Restarted clamav-freshclam.service to trigger database update")
1623+
);
1624+
return Ok(());
1625+
}
1626+
}
16111627
// Error! We add onto the error the output of running without sudo for more information.
16121628
Err(err.wrap_err(format!(
16131629
"Running `freshclam` with sudo failed as well as running without sudo. Output without sudo: {output:?}"

src/steps/os/dragonfly.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,21 @@ pub fn audit_packages(ctx: &ExecutionContext) -> Result<()> {
2121
print_separator(t!("DragonFly BSD Audit"));
2222

2323
let sudo = ctx.require_sudo()?;
24+
// Exit code 1 means the audit ran successfully but vulnerable packages remain.
25+
// Other non-zero exit codes indicate actual errors and should be propagated.
2426
sudo.execute(ctx, "/usr/local/sbin/pkg")?
2527
.args(["audit", "-Fr"])
2628
.status_checked_with(|status| {
27-
if !status.success() {
29+
if status.code() == Some(1) {
2830
println!(
2931
"{}",
3032
t!("The package audit was successful, but vulnerable packages still remain on the system")
3133
);
34+
Ok(())
35+
} else if status.success() {
36+
Ok(())
37+
} else {
38+
Err(())
3239
}
33-
Ok(())
3440
})
3541
}

src/steps/os/linux.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,11 @@ fn upgrade_debian(ctx: &ExecutionContext) -> Result<()> {
625625
command.status_checked()?;
626626
}
627627

628+
// Check if a reboot is required after apt upgrade
629+
if Path::new("/var/run/reboot-required").exists() {
630+
print_info(t!("A reboot is required to complete the system update."));
631+
}
632+
628633
Ok(())
629634
}
630635

@@ -931,11 +936,18 @@ fn should_skip_needrestart(ctx: &ExecutionContext) -> Result<()> {
931936
}
932937
}
933938
Distribution::Arch => {
939+
// Skip if needrestart is installed via pacman (its hook will run automatically)
934940
if let Some(manager) = get_arch_package_manager(ctx)
935941
&& manager.package_installed("needrestart", ctx)?
936942
{
937943
return Err(SkipStep(String::from(msg)).into());
938944
}
945+
// Also skip if needrestart pacman hook exists directly
946+
if Path::new("/etc/pacman.d/hooks/needrestart.hook").exists()
947+
|| Path::new("/usr/share/libalpm/hooks/needrestart.hook").exists()
948+
{
949+
return Err(SkipStep(String::from(msg)).into());
950+
}
939951
}
940952
_ => {}
941953
}
@@ -1000,7 +1012,11 @@ pub fn run_flatpak(ctx: &ExecutionContext) -> Result<()> {
10001012
if yes {
10011013
update_args.push("-y");
10021014
}
1003-
ctx.execute(&flatpak).args(&update_args).status_checked()?;
1015+
// Suppress ANSI escape codes that can clutter output in non-interactive contexts
1016+
ctx.execute(&flatpak)
1017+
.args(&update_args)
1018+
.env("NO_COLOR", "1")
1019+
.status_checked()?;
10041020

10051021
if cleanup {
10061022
let mut cleanup_args = vec!["uninstall", "--user", "--unused"];
@@ -1030,7 +1046,10 @@ pub fn run_flatpak(ctx: &ExecutionContext) -> Result<()> {
10301046
if yes {
10311047
update_args.push("-y");
10321048
}
1033-
ctx.execute(&flatpak).args(&update_args).status_checked()?;
1049+
ctx.execute(&flatpak)
1050+
.args(&update_args)
1051+
.env("NO_COLOR", "1")
1052+
.status_checked()?;
10341053
if cleanup {
10351054
let mut cleanup_args = vec!["uninstall", "--system", "--unused"];
10361055
if yes {
@@ -1186,7 +1205,7 @@ pub fn run_waydroid(ctx: &ExecutionContext) -> Result<()> {
11861205
.stdout
11871206
.lines()
11881207
.find(|line| line.contains("Session:"))
1189-
.unwrap_or_else(|| panic!("the output of `waydroid status` should contain `Session:`"));
1208+
.ok_or_else(|| SkipStep(t!("Could not parse waydroid status output (no Session line found)").to_string()))?;
11901209
let is_container_running = session.contains("RUNNING");
11911210
let assume_yes = ctx.config().yes(Step::Waydroid);
11921211

src/steps/os/unix.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,13 @@ pub fn run_atuin(ctx: &ExecutionContext) -> Result<()> {
11511151
}
11521152

11531153
pub fn reboot(ctx: &ExecutionContext) -> Result<()> {
1154+
// Prefer `systemctl reboot` on systemd-based systems
1155+
if Path::new("/run/systemd/system").exists() {
1156+
if let Ok(systemctl) = require("systemctl") {
1157+
return ctx.execute(systemctl).arg("reboot").status_checked();
1158+
}
1159+
}
1160+
11541161
match ctx.sudo() {
11551162
Some(sudo) => sudo.execute(ctx, "reboot")?.status_checked(),
11561163
None => ctx.execute("reboot").status_checked(),

src/steps/os/windows.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,17 @@ fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> R
175175
// not to the inner topgrade command (see comment above).
176176
let mut topgrade_args = Vec::new();
177177
if ctx.config().verbose() {
178-
topgrade_args.push("-v");
178+
topgrade_args.push("-v".to_string());
179179
}
180180
if ctx.config().yes(Step::Wsl) {
181-
topgrade_args.push("-y");
181+
topgrade_args.push("-y".to_string());
182182
}
183183
if ctx.config().cleanup() {
184-
topgrade_args.push("--cleanup");
184+
topgrade_args.push("--cleanup".to_string());
185+
}
186+
// Forward --disable flags to the WSL topgrade invocation
187+
for step in ctx.config().cli_disabled_steps() {
188+
topgrade_args.push(format!("--disable {step}"));
185189
}
186190
let args = topgrade_args.join(" ");
187191

src/steps/remote/ssh.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ pub fn ssh_step(ctx: &ExecutionContext, hostname: &str) -> Result<()> {
7171
// local instance instead of continuing with the remaining steps.
7272
let status = ctx.execute(ssh).args(&args).status_checked_with_codes_returning(&[2])?;
7373

74+
// Reset terminal title after remote execution (the remote may have changed it)
75+
print_separator(t!("Topgrade"));
76+
7477
if status.code() == Some(2) {
7578
return Err(io::Error::from(io::ErrorKind::Interrupted))
7679
.context("Remote topgrade quit by user (exit code 2)");

0 commit comments

Comments
 (0)