feat: add adaptive SMART refresh controls (#96)

This commit is contained in:
Gustavo Rosa
2026-07-24 14:13:17 -04:00
committed by GitHub
parent 27c6a808da
commit ff289e523f
14 changed files with 512 additions and 89 deletions
+31 -6
View File
@@ -26,13 +26,17 @@ local available = {
local rawFixture = {
schema = 2,
collector_version = mode == "outdated-raw-cache" and "0.6.0" or "2.0.0",
collector_version = mode == "outdated-raw-cache" and "0.6.0" or "2.0.1",
collection_id = "fixture-collection-id",
generated_at_epoch = 1700000000,
lsblk = { blockdevices = {} },
smart = {},
}
if mode == "raw-cache" or mode == "outdated-raw-cache" then
files["/usr/local/libexec/noctalia-drive-health/manage-collector.sh"] = "installed"
end
local function translate(key, substitutions)
local value = key
for name, replacement in pairs(substitutions or {}) do
@@ -47,6 +51,7 @@ noctalia = {
if key == "system_collector_enabled" then
return collectorEnabled
end
if key == "full_smart_refresh_minutes" then return 15 end
return nil
end,
pluginDir = function() return "/mock/plugin" end,
@@ -166,14 +171,14 @@ elseif mode == "probe-timeout" then
return
elseif mode == "missing-lsblk" then
assert(dependencies.ready == false and dependencies.blocking == true, "missing lsblk was not blocking")
assert(dependencies.install_command == "sudo pacman -S --needed util-linux", "wrong lsblk install command")
assert(dependencies.install_command == "pkexec pacman -S --needed --noconfirm util-linux", "wrong lsblk install command")
assert(launchedCommand == nil, "collector launched without lsblk")
assert(snapshot.collector_error ~= nil, "blocking dependency did not publish an error")
print("collector initialization test passed: " .. mode)
return
elseif mode == "missing-smartctl" then
assert(dependencies.ready == false and dependencies.blocking == false, "missing smartctl blocked inventory")
assert(dependencies.install_command == "sudo pacman -S --needed smartmontools", "wrong smartctl install command")
assert(dependencies.install_command == "pkexec pacman -S --needed --noconfirm smartmontools", "wrong smartctl install command")
assert(launchedCommand and launchedCommand:match("collect_raw%.sh"), "fallback collector did not launch")
print("collector initialization test passed: " .. mode)
return
@@ -181,8 +186,9 @@ elseif mode == "raw-cache" then
assert(snapshot.source == "system-cache", "raw cache was not normalized")
assert(snapshot.collection_id == "fixture-collection-id", "raw cache lost its collection ID")
assert(snapshot.system_collector.status == "healthy"
and snapshot.system_collector.version == "2.0.0"
and snapshot.system_collector.expected_version == "2.0.0",
and snapshot.system_collector.version == "2.0.1"
and snapshot.system_collector.expected_version == "2.0.1"
and snapshot.system_collector.smart_refresh_minutes == 15,
"current system collector was not reported healthy")
assert(not (launchedCommand or ""):match("collect_raw%.sh"), "collector launched despite a fresh raw cache")
print("collector initialization test passed: " .. mode)
@@ -191,7 +197,7 @@ elseif mode == "outdated-raw-cache" then
assert(snapshot.source == "system-cache", "outdated raw cache was not normalized")
assert(snapshot.system_collector.status == "upgrade-required"
and snapshot.system_collector.version == "0.6.0"
and snapshot.system_collector.expected_version == "2.0.0",
and snapshot.system_collector.expected_version == "2.0.1",
"older system collector did not request an upgrade")
assert(not (launchedCommand or ""):match("collect_raw%.sh"), "collector launched despite a fresh raw cache")
assert(#notifications == 1 and notifications[1].title == "collector.update_title"
@@ -465,6 +471,24 @@ local healthyRaw = assert(normalizeRaw({
assert(healthyRaw.disks[1].smart_available == true and healthyRaw.disks[1].smart_error == nil,
"healthy SMART data retained a contradictory error message")
directories["/sys/class/nvme/nvme0"] = { "hwmon0" }
files["/sys/class/nvme/nvme0/hwmon0/temp1_input"] = "51000\n"
local liveTemperatureRaw = assert(normalizeRaw({
schema = 2, generated_at_epoch = 1700000000,
lsblk = { blockdevices = { {
name = "nvme0n1", kname = "nvme0n1", path = "/dev/nvme0n1", type = "disk", tran = "nvme",
rota = false, size = 1000000000, model = "Live Temperature NVMe", serial = "LIVE1",
mountpoints = {}, children = {},
} } },
smart = { { requested_device = "/dev/nvme0", payload = {
smart_status = { passed = true }, temperature = { current = 42 },
} } },
}, "test"))
assert(liveTemperatureRaw.disks[1].temperature_c == 51
and liveTemperatureRaw.disks[1].hotspot_temperature_c == 51
and liveTemperatureRaw.disks[1].temperature_source == "sysfs",
"fresh sysfs temperature did not override the stale SMART-cache value")
local sleeping = assert(normalizeRaw({
schema = 2, generated_at_epoch = 1700000000,
lsblk = { blockdevices = { {
@@ -511,6 +535,7 @@ local oversizedId = assert(normalizeRaw({
assert(oversizedId.collection_id == nil, "oversized collection ID was preserved")
files["/usr/local/libexec/noctalia-drive-health/collect_raw.sh"] = "installed"
files["/usr/local/libexec/noctalia-drive-health/manage-collector.sh"] = "installed"
collectorEnabled = true
state.collector_snapshot = { summary = {}, system_collector = { status = "healthy" } }
publishError("fixture failure", { ready = true, blocking = false })
+60 -7
View File
@@ -77,10 +77,12 @@ state.snapshot = {
dependencies = { ready = true },
system_collector = { enabled = true, installed = true, status = "healthy", version = "1.0.0",
expected_version = "1.0.0", helper_available = true, authorization_available = true,
enable_command = "sudo systemctl enable --now noctalia-drive-health.timer",
disable_command = "sudo systemctl disable --now noctalia-drive-health.timer",
install_command = "sudo '/mock/plugin/packaging/install-system-collector.sh'",
uninstall_command = "sudo '/mock/plugin/packaging/uninstall-system-collector.sh'" },
enable_command = "pkexec '/usr/local/libexec/noctalia-drive-health/manage-collector.sh' start",
disable_command = "pkexec '/usr/local/libexec/noctalia-drive-health/manage-collector.sh' pause",
install_command = "pkexec '/mock/plugin/packaging/install-system-collector.sh' --interval-minutes 15",
uninstall_command = "pkexec '/usr/local/libexec/noctalia-drive-health/uninstall-collector.sh'",
interval_command = "pkexec '/usr/local/libexec/noctalia-drive-health/set-collector-interval.sh' 15",
smart_refresh_minutes = 15 },
summary = { disk_count = 2, ssd_count = 1, hdd_count = 1, smart_available_count = 2,
ssd_smart_available_count = 1, hottest_drive_temperature_c = 70,
hottest_drive_name = "Fixture SSD", hottest_ssd_temperature_c = 70,
@@ -180,8 +182,14 @@ assert(containsText(rendered, "collector.settings_title")
and containsText(rendered, "collector.full_features"),
"collector settings did not explain Basic and Full SMART capabilities")
onPauseCollectorClicked()
assert(terminalCommand == "sudo systemctl disable --now noctalia-drive-health.timer",
"collector settings did not expose the explicit service pause command")
assert(asyncCommand == "pkexec '/usr/local/libexec/noctalia-drive-health/manage-collector.sh' pause",
"collector settings did not use the fixed Polkit pause helper")
assert(terminalCommand == nil, "collector pause opened a terminal")
assert(containsText(rendered, "privileged_action.authorizing"),
"collector pause did not show authorization progress")
asyncCallback({ exitCode = 126, stdout = "", stderr = "", timedOut = false })
assert(errors[#errors].body:match("privileged_action.failed"),
"cancelled collector pause did not report a useful error")
terminalCommand = nil
onOpenPluginSettingsClicked()
assert(asyncCommand == "noctalia msg settings-open plugins",
@@ -383,10 +391,20 @@ onCancelDrivePreferencesClicked()
state.snapshot.dependencies = {
ready = false, blocking = true, missing_text = "lsblk (util-linux)",
install_command = "sudo pacman -S --needed util-linux", package_manager = "pacman", can_install = true,
install_command = "pkexec pacman -S --needed --noconfirm util-linux", package_manager = "pacman", can_install = true,
}
watchers.snapshot(state.snapshot)
assert(containsText(rendered, "dependencies.title"), "missing dependency card did not render")
asyncCommand = nil
asyncCallback = nil
terminalCommand = nil
onInstallDependenciesClicked()
assert(asyncCommand == "pkexec pacman -S --needed --noconfirm util-linux",
"dependency installation did not use Polkit")
assert(terminalCommand == nil, "dependency installation opened a terminal")
asyncCallback({ exitCode = 126, stdout = "", stderr = "", timedOut = false })
assert(errors[#errors].body:match("privileged_action.failed"),
"cancelled dependency installation did not report an error")
state.snapshot.dependencies = { ready = true }
state.snapshot.disks = { state.snapshot.disks[2] }
@@ -401,4 +419,39 @@ assert(not containsText(rendered, "summary.lowest_ssd_life"),
assert(countText(rendered, "Fixture HDD") >= 2,
"HDD-only temperature summary did not identify its drive")
-- Changing the full-SMART schedule uses Polkit in the background, rather than
-- placing a password-bearing command in a terminal. Keep the panel open so
-- the in-progress and error states are visible to the user.
onToggleCollectorSettingsClicked()
asyncCommand = nil
asyncCallback = nil
terminalCommand = nil
local refreshNonceBeforeInterval = state.refresh_nonce or 0
onApplyCollectorIntervalClicked()
assert(asyncCommand == "pkexec '/usr/local/libexec/noctalia-drive-health/set-collector-interval.sh' 15",
"collector interval update did not use the Polkit helper")
assert(terminalCommand == nil, "collector interval update opened a terminal")
assert(containsText(rendered, "collector.interval_authorizing"),
"collector interval authorization progress was not shown")
local intervalSuccess = assert(asyncCallback, "collector interval callback was not registered")
intervalSuccess({ exitCode = 0, stdout = "", stderr = "", timedOut = false })
assert((state.refresh_nonce or 0) == refreshNonceBeforeInterval + 1,
"successful collector interval update did not refresh the panel")
assert(notifications[#notifications].body == "collector.interval_applied",
"successful collector interval update did not notify the user")
onApplyCollectorIntervalClicked()
local intervalCancelled = assert(asyncCallback, "second collector interval callback was not registered")
intervalCancelled({ exitCode = 126, stdout = "", stderr = "", timedOut = false })
assert(containsText(rendered, "collector.interval_failed"),
"cancelled collector interval authorization did not render an inline result")
assert(errors[#errors].body == "collector.interval_cancelled",
"cancelled collector interval authorization did not notify the user")
onApplyCollectorIntervalClicked()
local intervalRejected = assert(asyncCallback, "failed collector interval callback was not registered")
intervalRejected({ exitCode = 1, stdout = "", stderr = "Authentication failed", timedOut = false })
assert(errors[#errors].body == "Authentication failed",
"collector interval authentication failure did not show the returned error")
print("panel rendering tests passed")
+1 -1
View File
@@ -7,7 +7,7 @@ fixture_bin="$project_dir/tests/fixtures/bin"
payload=$(PATH="$fixture_bin:$PATH" sh "$project_dir/scripts/collect_raw.sh")
printf '%s\n' "$payload" | jq -e '
.schema == 2
and .collector_version == "2.0.0"
and .collector_version == "2.0.1"
and (.collection_id | type == "string" and length > 0)
and ([.lsblk.blockdevices[].name] | sort) == ["/dev/nvme0n1", "/dev/sda", "/dev/zram0"]
and (.smart | length) == 2
+9 -1
View File
@@ -4,6 +4,8 @@ set -eu
project_dir=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
service_template="$project_dir/packaging/noctalia-drive-health.service.in"
timer="$project_dir/packaging/noctalia-drive-health.timer"
interval_script="$project_dir/packaging/set-collector-interval.sh"
manage_script="$project_dir/packaging/manage-collector.sh"
fixture=$(mktemp -d "${TMPDIR:-/tmp}/drive-health-packaging.XXXXXX")
trap 'rm -rf -- "$fixture"' EXIT HUP INT TERM
@@ -14,6 +16,12 @@ grep -q '^Group=1000$' "$fixture/noctalia-drive-health.service"
grep -q '^RuntimeDirectoryMode=0750$' "$fixture/noctalia-drive-health.service"
grep -q '^UMask=0027$' "$fixture/noctalia-drive-health.service"
grep -q '^Unit=noctalia-drive-health.service$' "$fixture/noctalia-drive-health.timer"
grep -q '^OnUnitActiveSec=15min$' "$fixture/noctalia-drive-health.timer"
bash -n "$interval_script"
bash -n "$manage_script"
grep -q 'packaging/manage-collector.sh' "$project_dir/packaging/install-system-collector.sh"
grep -q 'packaging/uninstall-system-collector.sh' "$project_dir/packaging/install-system-collector.sh"
if grep -R -q 'noctalia-smart-monito[r]' "$project_dir"; then
echo "generic legacy collector namespace must not be read, modified, or removed" >&2
@@ -27,7 +35,7 @@ fi
declared_dependencies=$(sed -n 's/^dependencies = \[\(.*\)\]$/\1/p' "$project_dir/plugin.toml")
for dependency in \
lsblk smartctl sh date dirname mkdir mktemp rm sed cat chmod mv sudo env bash \
lsblk smartctl sh date dirname mkdir mktemp rm sed cat chmod mv env bash \
install systemctl pkexec id tr pacman apt-get dnf zypper apk xbps-install emerge; do
case "$declared_dependencies" in
*\"$dependency\"*) ;;