Fix systemd unloaded unit

This commit is contained in:
2026-10-27 22:21:44 +02:00
parent 86c28a35b5
commit fe7e3d61c2
7 changed files with 763 additions and 43 deletions
+194 -8
View File
@@ -508,14 +508,32 @@ fn result_message(result: &CommandResult) -> String {
CommandResult::RestartPreview(_) => {
"Restart preview ready; confirm the exact saved-state action.".to_owned()
}
CommandResult::Restarted(report) => format!(
"Restart result: {:?}; failure: {:?}; submitted: {}; may still complete: {}; manual recovery required: {}.",
report.stage,
report.failure,
report.restart_submitted,
report.restart_may_still_complete,
report.manual_recovery_required
),
CommandResult::Restarted(report) => {
let mut message = format!(
"Restart result: {:?}; failure: {:?}; submitted: {}; may still complete: {}; manual recovery required: {}.",
report.stage,
report.failure,
report.restart_submitted,
report.restart_may_still_complete,
report.manual_recovery_required
);
if let Some(failure) = report.service_failure {
let phase = report
.service_phase
.map(restart_phase_label)
.unwrap_or("service operation");
let unit = report
.systemd_unit
.as_deref()
.map(render::safe_text)
.unwrap_or_else(|| "configured systemd unit".to_owned());
message.push_str(&format!(
" Systemd {phase} failed for {unit}: {}.",
restart_failure_label(failure)
));
}
message
}
CommandResult::DeliveryDestination(destination) => format!(
"Prepared destination {}. Overwrite confirmation required: {}.",
render::safe_text(&destination.path.display().to_string()),
@@ -566,6 +584,39 @@ fn result_message(result: &CommandResult) -> String {
}
}
fn restart_phase_label(phase: crate::service::RestartPhase) -> &'static str {
match phase {
crate::service::RestartPhase::Connect => "bus connection",
crate::service::RestartPhase::Subscribe => "job subscription",
crate::service::RestartPhase::ResolveUnit => "unit resolution",
crate::service::RestartPhase::RequestRestart => "restart request",
crate::service::RestartPhase::WaitForJob => "job wait",
}
}
fn restart_failure_label(failure: crate::service::RestartFailure) -> &'static str {
use crate::service::RestartFailure;
match failure {
RestartFailure::JobFailed => "job failed",
RestartFailure::JobDependency => "job dependency failed",
RestartFailure::JobTimedOut => "job timed out",
RestartFailure::JobCanceled => "job was canceled",
RestartFailure::JobSkipped => "job was skipped",
RestartFailure::JobResultUnknown => "job returned an unknown result",
RestartFailure::BusDisconnected => "D-Bus connection was lost",
RestartFailure::ManagerOwnerChanged => "systemd manager owner changed",
RestartFailure::SubscriptionFailed => "job subscription failed",
RestartFailure::UnitResolutionFailed => "unit could not be loaded",
RestartFailure::PermissionDenied => "permission denied",
RestartFailure::RequestFailed => "systemd request failed",
RestartFailure::WaitTimedOutMayStillComplete => {
"operation timed out and may still complete"
}
RestartFailure::EarlyEventBufferFull => "job event buffer was full",
RestartFailure::InvalidJobPath => "systemd returned an invalid job path",
}
}
fn process_command_result(
result: CommandResult,
state: &mut UiState,
@@ -1048,11 +1099,15 @@ mod tests {
#[test]
fn restart_results_render_cancel_success_and_uncertain_failure_honestly() {
use crate::app::{RestartFailureKind, RestartReport, RestartStage};
use crate::service::{RestartFailure, RestartPhase};
let report = |stage, failure, restart_submitted, restart_may_still_complete| {
CommandResult::Restarted(RestartReport {
stage,
failure,
service_failure: None,
service_phase: None,
systemd_unit: None,
backup_path: None,
restart_submitted,
restart_may_still_complete,
@@ -1076,6 +1131,137 @@ mod tests {
assert!(uncertain.contains("ServiceFailed"));
assert!(uncertain.contains("may still complete: true"));
assert!(uncertain.contains("manual recovery required: true"));
let detailed = CommandResult::Restarted(RestartReport {
stage: RestartStage::Service,
failure: Some(RestartFailureKind::ServiceFailed),
service_failure: Some(RestartFailure::PermissionDenied),
service_phase: Some(RestartPhase::ResolveUnit),
systemd_unit: Some("wireguard-vpn.service".to_owned()),
backup_path: None,
restart_submitted: false,
restart_may_still_complete: false,
manual_recovery_required: false,
});
let detailed = result_message(&detailed);
assert!(detailed.contains("unit resolution"));
assert!(detailed.contains("wireguard-vpn.service"));
assert!(detailed.contains("permission denied"));
assert!(!detailed.contains("org.freedesktop.DBus.Error"));
}
#[test]
fn every_redacted_service_failure_has_a_readable_tui_category_and_phase() {
use crate::app::{RestartFailureKind, RestartReport, RestartStage};
use crate::service::{RestartFailure, RestartPhase};
for (failure, phase, label) in [
(
RestartFailure::JobFailed,
RestartPhase::WaitForJob,
"job failed",
),
(
RestartFailure::JobDependency,
RestartPhase::WaitForJob,
"job dependency failed",
),
(
RestartFailure::JobTimedOut,
RestartPhase::WaitForJob,
"job timed out",
),
(
RestartFailure::JobCanceled,
RestartPhase::WaitForJob,
"job was canceled",
),
(
RestartFailure::JobSkipped,
RestartPhase::WaitForJob,
"job was skipped",
),
(
RestartFailure::JobResultUnknown,
RestartPhase::WaitForJob,
"job returned an unknown result",
),
(
RestartFailure::BusDisconnected,
RestartPhase::Connect,
"D-Bus connection was lost",
),
(
RestartFailure::ManagerOwnerChanged,
RestartPhase::WaitForJob,
"systemd manager owner changed",
),
(
RestartFailure::SubscriptionFailed,
RestartPhase::Subscribe,
"job subscription failed",
),
(
RestartFailure::UnitResolutionFailed,
RestartPhase::ResolveUnit,
"unit could not be loaded",
),
(
RestartFailure::PermissionDenied,
RestartPhase::RequestRestart,
"permission denied",
),
(
RestartFailure::RequestFailed,
RestartPhase::RequestRestart,
"systemd request failed",
),
(
RestartFailure::WaitTimedOutMayStillComplete,
RestartPhase::WaitForJob,
"operation timed out and may still complete",
),
(
RestartFailure::EarlyEventBufferFull,
RestartPhase::WaitForJob,
"job event buffer was full",
),
(
RestartFailure::InvalidJobPath,
RestartPhase::WaitForJob,
"systemd returned an invalid job path",
),
] {
let report = CommandResult::Restarted(RestartReport {
stage: RestartStage::Service,
failure: Some(RestartFailureKind::ServiceFailed),
service_failure: Some(failure),
service_phase: Some(phase),
systemd_unit: Some("wg-quick@wg0.service".to_owned()),
backup_path: None,
restart_submitted: true,
restart_may_still_complete: true,
manual_recovery_required: true,
});
let message = result_message(&report);
assert!(message.contains(label), "missing {failure:?}: {message}");
assert!(message.contains("wg-quick@wg0.service"));
assert!(message.contains(restart_phase_label(phase)));
assert!(!message.contains("org.freedesktop.DBus.Error"));
}
let hostile_unit = CommandResult::Restarted(RestartReport {
stage: RestartStage::Service,
failure: Some(RestartFailureKind::ServiceFailed),
service_failure: Some(RestartFailure::PermissionDenied),
service_phase: Some(RestartPhase::ResolveUnit),
systemd_unit: Some("wg0\u{1b}[31m.service".to_owned()),
backup_path: None,
restart_submitted: false,
restart_may_still_complete: false,
manual_recovery_required: false,
});
assert!(!result_message(&hostile_unit).contains('\u{1b}'));
}
#[test]