Plasmoide CPU Frequency Control: governor, limiti, EPP, boost e profili per task

Widget KDE Plasma 6 (org.enne2.cpufreq) per pilotare il profilo energetico della
CPU: governor, limiti min/max, energy_performance_preference, boost/turbo,
quattro profili pronti (Silenzioso, Risparmio, Bilanciato, Performance) e task
con profilo dedicato che ripristina lo stato precedente alla fine del comando.

Privilegi separati: il widget resta non privilegiato e l'unica operazione root
passa da un helper a interfaccia ristretta in /usr/local/libexec/enne2-cpufreq/
(comandi a elenco chiuso, argomenti validati su sysfs, rilettura di verifica),
invocato via pkexec con l'azione polkit dedicata org.enne2.cpufreq.set. La regola
in /etc/polkit-1/rules.d/ concede il consenso senza prompt a utenti locali in
sessione attiva nel gruppo wheel; senza la regola la policy chiede la password.

Dettagli verificati su frigate (Plasma 6.7.5, kernel 7.2, amd-pstate-epp):
- con boost disattivato cpuinfo_max_freq scende alla frequenza nominale, quindi
  il tetto hardware si legge da amd_pstate_max_freq e i profili applicano il
  boost prima dei limiti;
- scrittura dei limiti in ordine sicuro (mai min > max intermedio);
- baseline e contatore di riferimento per i task paralleli;
- syncControls protetto da uiReady e try/catch: un ReferenceError su un id della
  rappresentazione non deve interrompere l'aggiornamento dello stato;
- implicitWidth/Height costanti, altrimenti il containment riduce il widget a
  68x68.

Include install.sh/uninstall.sh (widget, backend, dry-run), README con modello di
sicurezza e diagnostica, screenshot docs/widget.png. Licenza MIT.
This commit is contained in:
enne2
2026-10-25 20:42:52 +01:00
commit d4902b0b72
15 changed files with 2674 additions and 0 deletions
+274
View File
@@ -0,0 +1,274 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# enne2-cpufreq-task — launcher di task con profilo CPU dedicato.
#
# Gira SEMPRE come utente: il comando del task non viene mai eseguito come root.
# Solo il cambio di profilo passa dall'helper privilegiato via pkexec.
#
# Flusso di "run":
# 1. helper task-begin <profilo> → salva la baseline (una sola volta) e applica
# 2. il comando parte staccato (setsid) e scrive il proprio pid nel file di stato
# 3. un watcher staccato attende l'uscita del pid e chiama helper task-end, che
# ripristina la baseline quando non ci sono altri task attivi
#
# Il plasmoide invoca "add-hex" con gli argomenti codificati in esadecimale
# (UTF-8): così nomi, comandi con spazi, virgolette o newline attraversano
# l'exec engine di Plasma senza dipendere dal parsing delle virgolette.
set -u
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
export PATH
SELF_DIR=$(dirname "$0")
HELPER=/usr/local/libexec/enne2-cpufreq/cpufreq-helper
CONF_DIR=${XDG_CONFIG_HOME:-$HOME/.config}/enne2-cpufreq
TASKS=$CONF_DIR/tasks.json
RUN=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/enne2-cpufreq
mkdir -p "$CONF_DIR" "$RUN" 2>/dev/null
fail() {
printf 'ok=0\nerror=%s\n' "$*"
exit 2
}
readf() {
[ -r "$1" ] || return 1
_val=
IFS= read -r _val < "$1" 2>/dev/null || [ -n "$_val" ] || return 1
printf '%s\n' "$_val"
}
# Decodifica esadecimale UTF-8 senza dipendenze (xxd/od non sempre presenti).
hex_decode() {
[ -n "${1:-}" ] || return 0
case "$1" in *[!0-9a-fA-F]*) fail "payload esadecimale non valido" ;; esac
_esc=$(printf '%s' "$1" | sed 's/\(..\)/\\x\1/g')
printf '%b' "$_esc"
}
need_jq() {
command -v jq >/dev/null 2>&1 || fail "jq non installato (serve per gestire tasks.json)"
}
init_tasks() {
[ -r "$TASKS" ] && return 0
printf '{"version":1,"tasks":[]}\n' > "$TASKS" || fail "impossibile creare $TASKS"
}
slugify() {
printf '%s' "$1" | tr 'A-Z' 'a-z' | tr -c 'A-Za-z0-9._-' '-' \
| sed -e 's/--*/-/g' -e 's/^-//' -e 's/-$//'
}
# Accetta indifferentemente il nome visualizzato o lo slug.
resolve_slug() {
jq -r --arg q "$1" '.tasks[] | select(.name == $q or .slug == $q) | .slug' \
"$TASKS" 2>/dev/null | head -n 1
}
field_of_slug() { # field_of_slug <slug> <campo>
jq -r --arg s "$1" --arg f "$2" \
'.tasks[] | select(.slug == $s) | .[$f] // empty' "$TASKS" 2>/dev/null | head -n 1
}
is_running() { # is_running <slug>
_pid=$(readf "$RUN/$1.pid") || return 1
case "$_pid" in ''|*[!0-9]*) return 1 ;; esac
kill -0 "$_pid" 2>/dev/null
}
helper_ready() {
[ -x "$HELPER" ] || fail "helper non installato in $HELPER (eseguire install.sh)"
command -v pkexec >/dev/null 2>&1 || fail "pkexec non disponibile (installare polkit)"
}
do_add() { # do_add <nome> <profilo> <comando>
init_tasks
need_jq
_name=${1:-}
_prof=${2:-}
_cmd=${3:-}
[ -n "$_name" ] || fail "nome del task mancante"
[ -n "$_cmd" ] || fail "comando del task mancante"
case $_prof in
quiet|powersave|balanced|performance) ;;
*) fail "profilo non valido: $_prof (quiet|powersave|balanced|performance)" ;;
esac
_slug=$(slugify "$_name")
[ -n "$_slug" ] || fail "nome non utilizzabile come slug: $_name"
if [ -n "$(resolve_slug "$_name")" ]; then
fail "esiste già un task chiamato '$_name'"
fi
_tmp=$TASKS.tmp.$$
jq --arg n "$_name" --arg p "$_prof" --arg c "$_cmd" --arg s "$_slug" \
'.tasks += [{name: $n, profile: $p, command: $c, slug: $s}]' "$TASKS" > "$_tmp" \
&& mv "$_tmp" "$TASKS" || { rm -f "$_tmp"; fail "scrittura tasks.json fallita"; }
printf 'ok=1\nname=%s\nprofile=%s\nslug=%s\n' "$_name" "$_prof" "$_slug"
}
cmd_add() { # add <nome> <profilo> <comando...>
_name=${1:-}
_prof=${2:-}
[ "$#" -ge 3 ] || fail "servono nome, profilo e comando"
shift 2
do_add "$_name" "$_prof" "$*"
}
cmd_add_hex() { # add-hex <nome_hex> <profilo_hex> <comando_hex>
[ "$#" -ge 3 ] || fail "servono tre argomenti esadecimali"
do_add "$(hex_decode "$1")" "$(hex_decode "$2")" "$(hex_decode "$3")"
}
cmd_list() {
init_tasks
need_jq
printf 'ok=1\n'
[ -x "$HELPER" ] && printf 'helper=ok\n' || printf 'helper=missing\n'
printf 'count=%s\n' "$(jq -r '.tasks | length' "$TASKS" 2>/dev/null || echo 0)"
_i=0
jq -c '.tasks[] | {name: .name, profile: .profile, command: .command, slug: .slug}' \
"$TASKS" 2>/dev/null |
while IFS= read -r _line; do
[ -n "$_line" ] || continue
_slug=$(printf '%s' "$_line" | jq -r '.slug')
_run=0
is_running "$_slug" && _run=1
printf 'task=%s\t%s\t%s\n' "$_i" "$_run" "$_line"
_i=$((_i + 1))
done
}
cmd_remove() { # remove <nome-o-slug>
init_tasks
need_jq
_slug=$(resolve_slug "${1:-}")
[ -n "$_slug" ] || fail "task '${1:-}' inesistente"
is_running "$_slug" && fail "il task è in esecuzione: fermarlo prima di rimuoverlo"
_tmp=$TASKS.tmp.$$
jq --arg s "$_slug" '.tasks |= map(select(.slug != $s))' "$TASKS" > "$_tmp" \
&& mv "$_tmp" "$TASKS" || { rm -f "$_tmp"; fail "scrittura tasks.json fallita"; }
rm -f "$RUN/$_slug.pid" "$RUN/$_slug.info"
printf 'ok=1\nremoved=%s\n' "$_slug"
}
cmd_run() { # run <nome-o-slug>
init_tasks
need_jq
helper_ready
_slug=$(resolve_slug "${1:-}")
[ -n "$_slug" ] || fail "task '${1:-}' inesistente"
_name=$(field_of_slug "$_slug" name)
_prof=$(field_of_slug "$_slug" profile)
_cmd=$(field_of_slug "$_slug" command)
[ -n "$_cmd" ] || fail "comando vuoto per '$_name'"
is_running "$_slug" && fail "il task '$_name' è già in esecuzione"
_out=$(pkexec "$HELPER" task-begin "$_prof" 2>&1) \
|| fail "profilo '$_prof' non applicato: $(printf '%s' "$_out" | tr '\n' ' ')"
case "$_out" in
*"ok=0"*) fail "profilo '$_prof' non applicato: $(printf '%s' "$_out" | tr '\n' ' ')" ;;
esac
rm -f "$RUN/$_slug.pid" "$RUN/$_slug.info"
# Il figlio scrive il proprio pid e poi exec il comando: il pid resta valido,
# così il watcher può attenderne davvero la fine.
setsid sh -c 'printf "%s\n" "$$" > "$1"; shift; exec sh -c "$1"' \
sh "$RUN/$_slug.pid" "$_cmd" </dev/null >/dev/null 2>&1 &
_launcher=$!
_n=0
while [ ! -s "$RUN/$_slug.pid" ] && [ "$_n" -lt 25 ]; do
sleep 0.1 2>/dev/null || sleep 1
_n=$((_n + 1))
kill -0 "$_launcher" 2>/dev/null || break
done
if [ ! -s "$RUN/$_slug.pid" ]; then
pkexec "$HELPER" task-end >/dev/null 2>&1
fail "avvio del comando fallito: $_cmd"
fi
_pid=$(readf "$RUN/$_slug.pid")
printf 'name=%s\nprofile=%s\nslug=%s\nstarted=%s\n' \
"$_name" "$_prof" "$_slug" "$(date +%s)" > "$RUN/$_slug.info"
setsid "$SELF_DIR/$(basename "$0")" watch "$_slug" </dev/null >/dev/null 2>&1 &
printf 'ok=1\nname=%s\nslug=%s\nprofile=%s\npid=%s\n' "$_name" "$_slug" "$_prof" "$_pid"
}
cmd_stop() { # stop <nome-o-slug>
init_tasks
need_jq
_slug=$(resolve_slug "${1:-}")
[ -n "$_slug" ] || fail "task '${1:-}' inesistente"
is_running "$_slug" || fail "il task '$_slug' non è in esecuzione"
_pid=$(readf "$RUN/$_slug.pid")
kill -TERM -- "-$_pid" 2>/dev/null || kill -TERM "$_pid" 2>/dev/null
printf 'ok=1\nstopped=%s\npid=%s\n' "$_slug" "$_pid"
}
cmd_watch() { # watch <slug> (interno)
_slug=${1:-}
[ -n "$_slug" ] || exit 2
_pid=$(readf "$RUN/$_slug.pid" 2>/dev/null) || exit 0
while kill -0 "$_pid" 2>/dev/null; do
sleep 2
done
_name=$(sed -n 's/^name=//p' "$RUN/$_slug.info" 2>/dev/null)
_prof=$(sed -n 's/^profile=//p' "$RUN/$_slug.info" 2>/dev/null)
rm -f "$RUN/$_slug.pid" "$RUN/$_slug.info"
if [ -x "$HELPER" ]; then
pkexec "$HELPER" task-end >/dev/null 2>&1
fi
if command -v notify-send >/dev/null 2>&1; then
notify-send -a "CPUFreq" -i cpu \
"Task concluso: ${_name:-$_slug}" \
"Profilo CPU '${_prof:-?}' chiuso, stato precedente ripristinato"
fi
}
cmd_active() {
printf 'ok=1\n'
_found=0
for _f in "$RUN"/*.pid; do
[ -r "$_f" ] || continue
_pid=$(readf "$_f")
case "$_pid" in ''|*[!0-9]*) continue ;; esac
kill -0 "$_pid" 2>/dev/null || continue
_slug=$(basename "$_f" .pid)
_name=$(sed -n 's/^name=//p' "$RUN/$_slug.info" 2>/dev/null)
_prof=$(sed -n 's/^profile=//p' "$RUN/$_slug.info" 2>/dev/null)
printf 'act=%s\t%s\t%s\t%s\n' "$_slug" "${_name:-$_slug}" "${_prof:-?}" "$_pid"
_found=$((_found + 1))
done
printf 'active=%s\n' "$_found"
}
usage() {
cat <<'EOF'
uso: enne2-cpufreq-task <comando> [argomenti]
list elenco dei task configurati
add <nome> <profilo> <comando...>
add-hex <nome> <profilo> <comando> argomenti in esadecimale UTF-8 (usato dal widget)
remove <nome-o-slug>
run <nome-o-slug> salva lo stato, applica il profilo, esegue, ripristina
stop <nome-o-slug> termina il task (il watcher ripristina il profilo)
active task attualmente in esecuzione
EOF
}
[ "$#" -gt 0 ] || { usage; exit 2; }
_CMD=$1
shift
case "$_CMD" in
list) cmd_list ;;
add) cmd_add "$@" ;;
add-hex) cmd_add_hex "$@" ;;
remove|rm) cmd_remove "$@" ;;
run|start) cmd_run "$@" ;;
stop) cmd_stop "$@" ;;
watch) cmd_watch "$@" ;;
active) cmd_active ;;
-h|--help|help) usage ;;
*) printf 'ok=0\nerror=comando sconosciuto: %s\n' "$_CMD"; usage; exit 2 ;;
esac