Skip to content

Commit 00a7211

Browse files
committed
feat: implement X-RestartIfChanged=false in service activation
Prevents the engine from restarting services like the auto-upgrade service mid-execution when their unit files change.
1 parent 0c7aa7d commit 00a7211

3 files changed

Lines changed: 107 additions & 5 deletions

File tree

crates/system-manager-engine/src/activate/services.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,16 @@ pub struct ServiceConfig {
1818
store_path: Option<StorePath>,
1919
#[serde(default)]
2020
masked: bool,
21+
#[serde(default = "default_restart_if_changed")]
22+
restart_if_changed: bool,
2123
}
2224

2325
pub type Services = HashMap<String, ServiceConfig>;
2426

27+
const fn default_restart_if_changed() -> bool {
28+
true
29+
}
30+
2531
fn print_services(services: &Services) -> String {
2632
let out = itertools::intersperse(
2733
services.iter().map(|(name, entry)| {
@@ -134,7 +140,14 @@ fn get_services_to_reload(services: Services, old_services: Services) -> Service
134140
return false;
135141
}
136142
if let Some(old_service) = old_services.get(name) {
137-
service.store_path != old_service.store_path
143+
if service.store_path == old_service.store_path {
144+
return false;
145+
}
146+
if !service.restart_if_changed {
147+
log::info!("Skipping restart of {name}: X-RestartIfChanged=false");
148+
return false;
149+
}
150+
true
138151
} else {
139152
// Since we run this on the intersection, this should never happen
140153
panic!("Something went terribly wrong!");

nix/modules/default.nix

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -335,10 +335,15 @@
335335

336336
activeServices = lib.mapAttrs' (
337337
unitName: unit:
338-
lib.nameValuePair unitName {
339-
storePath = "${unit.unit}/${unitName}";
340-
masked = false;
341-
}
338+
lib.nameValuePair unitName (
339+
{
340+
storePath = "${unit.unit}/${unitName}";
341+
masked = false;
342+
}
343+
// lib.optionalAttrs (unit ? restartIfChanged) {
344+
inherit (unit) restartIfChanged;
345+
}
346+
)
342347
) enabledUnits;
343348

344349
maskedServices = lib.listToAttrs (
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
{
2+
forEachDistro,
3+
system-manager,
4+
system,
5+
...
6+
}:
7+
8+
let
9+
makeConfig =
10+
envValue:
11+
system-manager.lib.makeSystemConfig {
12+
modules = [
13+
(
14+
{ lib, ... }:
15+
{
16+
nixpkgs.hostPlatform = system;
17+
system-manager.allowAnyDistro = true;
18+
19+
systemd.services.long-running-task = {
20+
description = "Long Running Task";
21+
wantedBy = [ "system-manager.target" ];
22+
restartIfChanged = false;
23+
serviceConfig.Type = "simple";
24+
serviceConfig.Environment = "FOO=${envValue}";
25+
script = ''
26+
sleep infinity
27+
'';
28+
};
29+
}
30+
)
31+
];
32+
};
33+
34+
configV1 = makeConfig "v1";
35+
configV2 = makeConfig "v2";
36+
in
37+
38+
forEachDistro "restart-if-changed" {
39+
modules = [
40+
{
41+
systemd.services.long-running-task = {
42+
description = "Long Running Task";
43+
wantedBy = [ "system-manager.target" ];
44+
restartIfChanged = false;
45+
serviceConfig.Type = "simple";
46+
serviceConfig.Environment = "FOO=v1";
47+
script = ''
48+
sleep infinity
49+
'';
50+
};
51+
}
52+
];
53+
extraPathsToRegister = [ configV2 ];
54+
testScriptFunction =
55+
{ toplevel, hostPkgs, ... }:
56+
''
57+
start_all()
58+
59+
machine.wait_for_unit("multi-user.target")
60+
61+
with subtest("activate configV1 and verify service is running"):
62+
activation_logs = machine.activate()
63+
machine.wait_for_unit("system-manager.target")
64+
result = machine.succeed("systemctl is-active long-running-task.service").strip()
65+
assert result == "active", f"Expected active, got {result}"
66+
67+
with subtest("record initial PID"):
68+
pid_before = machine.succeed("systemctl show -p MainPID --value long-running-task.service").strip()
69+
assert pid_before != "0", "Service should have a non-zero PID"
70+
71+
with subtest("activate configV2 with X-RestartIfChanged=false"):
72+
activation_logs = machine.activate(profile="${configV2}")
73+
machine.wait_for_unit("system-manager.target")
74+
75+
with subtest("service was not restarted (same PID)"):
76+
pid_after = machine.succeed("systemctl show -p MainPID --value long-running-task.service").strip()
77+
assert pid_after == pid_before, \
78+
f"Service was restarted: PID changed from {pid_before} to {pid_after}"
79+
80+
with subtest("activation logs show skip message"):
81+
assert "Skipping restart" in activation_logs and "long-running-task" in activation_logs, \
82+
f"Expected skip message in logs, got: {activation_logs}"
83+
'';
84+
}

0 commit comments

Comments
 (0)