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
+12
View File
@@ -0,0 +1,12 @@
# Artefatti Plasma / Qt
*.plasmoid
*.qmlc
*.jsc
build/
package/
# Editor e sistema
*~
*.swp
.DS_Store
*.backup-*
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 enne2
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+163
View File
@@ -0,0 +1,163 @@
# CPU Frequency Control (org.enne2.cpufreq)
Plasmoide KDE Plasma 6 per pilotare il profilo energetico della CPU: governor,
limiti di frequenza (min/max), `energy_performance_preference` (EPP), boost/turbo,
quattro profili pronti e **task con profilo dedicato** che ripristina lo stato
precedente quando il comando finisce.
Verificato su **frigate** (Fedora 44, Plasma 6.7.5 Wayland, kernel 7.2, AMD
Ryzen 7 H 255, driver `amd-pstate-epp` in modalità `active`, 16 policy).
![widget](docs/widget.png)
## Cosa fa
| Area | Azione |
|---|---|
| Profili | **Silenzioso** (EPP power, boost off, tetto 35%), **Risparmio** (balance_power, boost off, tetto 60%), **Bilanciato** (balance_performance, boost on, nessun tetto), **Performance** (governor performance, EPP performance, boost on) |
| Governor | pillole con i valori realmente disponibili in `scaling_available_governors` |
| EPP | pillole con i valori di `energy_performance_available_preferences` |
| Limiti | due slider min/max in kHz con validazione e rilettura; il valore applicato è arrotondato a 10 MHz |
| Boost | pillole `on`/`off` (per-policy su kernel recenti, altrimenti globale, altrimenti `intel_pstate/no_turbo`) |
| Task | elenco configurabile: ogni task applica un profilo, esegue il comando **come utente** e ripristina la baseline all'uscita |
| Footer | messaggio dell'ultima azione e pulsante **Reset** (default hardware: boost on, nessun tetto) |
La vista compatta (pannello) mostra frequenza corrente e un badge `no boost`.
La frequenza è la massima fra le policy (core più attivo), con la media sotto.
## Architettura e modello di sicurezza
```
org.enne2.cpufreq (QML, non privilegiato)
├─ contents/scripts/cpufreq-state.sh lettura sysfs, /proc, hwmon (nessun privilegio)
├─ contents/scripts/enne2-cpufreq-task launcher task (utente)
└─ pkexec /usr/local/libexec/enne2-cpufreq/cpufreq-helper (root)
├─ comandi a elenco chiuso, argomenti validati contro sysfs
├─ ogni scrittura riletta per verifica
└─ /var/lib/enne2-cpufreq/{baseline,refcount} (root:root 0755/0644)
polkit
├─ /usr/share/polkit-1/actions/org.enne2.cpufreq.set.policy azione dedicata
└─ /etc/polkit-1/rules.d/50-org.enne2.cpufreq.rules allow senza prompt
```
- Il widget non è mai root: l'unica operazione privilegiata è l'helper, con
sottocomandi fissi (`set-governor`, `set-epp`, `set-limits`, `set-boost`,
`apply`, `reset`, `task-begin`, `task-end`, `save-state`, `restore-state`).
- L'annotazione `org.freedesktop.policykit.exec.path` lega l'azione polkit a quel
solo binario: nessun altro programma eredita il permesso.
- La regola in `/etc/polkit-1/rules.d/` concede `polkit.Result.YES` solo a utenti
**locali**, con sessione **attiva**, nel gruppo **wheel**. Senza quel file la
`.policy` chiede la password a ogni modifica (`auth_admin_keep`).
- I comandi dei task girano come utente (`setsid sh -c`), mai come root.
## Installazione
```sh
cd ~/Dev/kde-cpufreq-dashboard
./install.sh # widget (utente) + backend (sudo)
./install.sh --widget # solo il plasmoide
./install.sh --backend # solo helper + polkit (root)
./install.sh --no-polkit-rule # backend senza la regola "nessun prompt"
./install.sh --dry-run # mostra le operazioni
```
Il widget si aggiunge da *Aggiungi widget → «CPU Frequency Control»* oppure via
scripting Plasma:
```js
desktops()[0].addWidget("org.enne2.cpufreq")
```
Disinstallazione: `./uninstall.sh` (ripristina la baseline prima di rimuovere
l'helper). I task configurati restano in `~/.config/enne2-cpufreq/tasks.json`.
## Task con profilo dedicato
Dall'interfaccia: **Nuovo** → nome, comando, profilo. Da terminale:
```sh
enne2-cpufreq-task list
enne2-cpufreq-task add "Render Blender" performance "blender -b scena.blend -f 1"
enne2-cpufreq-task run render-blender
enne2-cpufreq-task stop render-blender
enne2-cpufreq-task active
```
Sequenza di `run`:
1. `helper task-begin <profilo>`: salva la baseline (solo al primo task) e applica il profilo
2. il comando parte staccato (`setsid`) e scrive il proprio pid in
`$XDG_RUNTIME_DIR/enne2-cpufreq/<slug>.pid`
3. un watcher attende l'uscita del pid e chiama `helper task-end`, che decrementa
un contatore e ripristina la baseline a zero (con `notify-send` di conferma)
Più task in parallelo sono contati: il ripristino avviene solo quando l'ultimo
termina. `enne2-cpufreq-task remove` rifiuta di rimuovere un task in esecuzione.
## Comportamento sui driver
- **amd-pstate-epp (active)**: `powersave`/`performance` sono policy del driver;
`powersave` *non* blocca la frequenza al minimo, EPP è il vero bias.
Con `boost=0` il kernel abbassa `cpuinfo_max_freq` alla frequenza nominale
(3.80 GHz invece di 4.97 GHz): il tetto reale viene letto da
`amd_pstate_max_freq`, e per questo `apply` imposta **prima** il boost e poi i
limiti.
- **intel_pstate**: boost tramite `intel_pstate/no_turbo` (invertito).
- **Altri driver cpufreq**: governor e limiti restano validi; EPP e boost
compaiono solo se le interfacce esistono.
## Servizi concorrenti
Se `tuned`, `power-profiles-daemon` o `tlp` sono attivi il widget lo segnala: il
profilo `balanced` di tuned scrive esattamente `governor`, `energy_performance_preference`
e `boost`, quindi un `tuned-adm profile` o il boot possono sovrascrivere le
impostazioni. Su frigate `tuned` è attivo: le modifiche valgono fino al prossimo
apply di tuned.
## Diagnosi
```sh
/usr/local/libexec/enne2-cpufreq/cpufreq-helper status # stato, baseline, refcount
~/.local/share/plasma/plasmoids/org.enne2.cpufreq/contents/scripts/cpufreq-state.sh
pkexec /usr/local/libexec/enne2-cpufreq/cpufreq-helper apply balanced
journalctl --user -u plasma-plasmashell.service -f | grep cpufreq
```
| Sintomo | Causa |
|---|---|
| banner rosso «Backend non installato» | manca `/usr/local/libexec/enne2-cpufreq/` → `./install.sh --backend` |
| dialog password a ogni modifica | manca la regola polkit (o utente fuori da `wheel`) |
| «autorizzazione negata da polkit» | regola non caricata: `systemctl try-reload-or-restart polkit` |
| valori che tornano indietro | servizio concorrente (tuned/power-profiles-daemon/tlp) |
| il widget resta 68×68 | `implicitWidth/Height` del plasmoide non validi: verificare `main.qml` |
## Note di implementazione Plasma 6
- `metadata.json` con `X-Plasma-API-Minimum-Version: "6.0"`, senza chiavi Plasma 5.
- Comandi via `org.kde.plasma.plasma5support` (`engine: "executable"`); ogni
azione usa un source univoco (contatore in coda) perché il motore non riesegue
un source già noto.
- `syncControls()` è protetto: `applyState()` gira ogni 2 s e la rappresentazione
piena può non esistere (popup chiuso) → senza guardia il `ReferenceError` su un
id interrompeva la sincronizzazione dei controlli.
- `implicitWidth/implicitHeight` del `Flickable` sono costanti: se dipendono dal
layout il containment riduce il widget alla dimensione minima (68×68).
- Nessun `Switch` per il boost: `toggled` scatta anche quando il valore viene
riallineato dallo stato. Le pillole sono prive di stato interno.
## File
```
metadata.json
contents/ui/main.qml interfaccia (compact + full, Flickable)
contents/ui/ChoicePill.qml pillola selezionabile
contents/scripts/profiles.sh tabella profili condivisa (unica fonte)
contents/scripts/cpufreq-state.sh lettura stato non privilegiata
contents/scripts/enne2-cpufreq-task launcher task (utente)
bin/cpufreq-helper helper privilegiato (root)
polkit/*.policy, polkit/*.rules azione e regola polkit
install.sh, uninstall.sh
```
Licenza MIT.
+410
View File
@@ -0,0 +1,410 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# enne2-cpufreq — helper privilegiato a interfaccia ristretta.
#
# Installato come /usr/local/libexec/enne2-cpufreq/cpufreq-helper (root:root 0755),
# invocato dal plasmoide org.enne2.cpufreq tramite pkexec e una azione polkit
# dedicata (org.enne2.cpufreq.set). Non accetta comandi arbitrari: solo i
# sottocomandi elencati in usage() e con argomenti validati contro sysfs.
#
# Uscita: righe key=value; ok=1 in caso di successo, ok=0 + error=<msg> + exit 2
# in caso di rifiuto. Ogni scrittura viene riletta per verifica.
set -u
PATH=/usr/sbin:/usr/bin:/sbin:/bin
export PATH
umask 022
CMD=${1:-status}
[ "$#" -gt 0 ] && shift
SELF_DIR=$(dirname "$0")
if [ -r "$SELF_DIR/profiles.sh" ]; then
# shellcheck source=../../contents/scripts/profiles.sh
. "$SELF_DIR/profiles.sh"
else
printf 'ok=0\nerror=profiles.sh mancante accanto all helper\n'
exit 2
fi
CPUFREQ=/sys/devices/system/cpu/cpufreq
INTEL_PSTATE=/sys/devices/system/cpu/intel_pstate
STATE_DIR=/var/lib/enne2-cpufreq
BASELINE=$STATE_DIR/baseline
REFCOUNT=$STATE_DIR/refcount
usage() {
cat <<'EOF'
uso: cpufreq-helper <comando> [argomenti]
status stato corrente (leggibile anche senza root)
set-governor <governor> powersave | performance | ...
set-epp <preferenza> default | performance | balance_performance | balance_power | power
set-limits <min_khz> <max_khz>
set-boost <0|1>
apply <profilo> quiet | powersave | balanced | performance
reset ripristina i default hardware (boost on, nessun tetto)
save-state salva lo stato corrente come baseline
restore-state ripristina la baseline salvata
task-begin <profilo> salva la baseline (se serve) e applica il profilo
task-end [--all] decrementa il contatore e ripristina la baseline a zero
EOF
}
fail() {
printf 'ok=0\nerror=%s\n' "$*"
exit 2
}
readf() { # readf <file> -> valore o exit 1
[ -r "$1" ] || return 1
_val=
IFS= read -r _val < "$1" 2>/dev/null || [ -n "$_val" ] || return 1
printf '%s\n' "$_val"
}
policies() {
for _p in "$CPUFREQ"/policy[0-9]*; do
[ -d "$_p" ] && printf '%s\n' "$_p"
done
}
write_one() { # write_one <file> <valore>
[ -w "$1" ] || return 1
printf '%s\n' "$2" > "$1" 2>/dev/null
}
# Scrive un attributo su TUTTE le policy. Imposta WROTE e FAILED.
write_all() {
_attr=$1
_val=$2
WROTE=0
FAILED=0
for _p in $(policies); do
if write_one "$_p/$_attr" "$_val"; then
WROTE=$((WROTE + 1))
else
FAILED=$((FAILED + 1))
fi
done
[ "$FAILED" -eq 0 ] && [ "$WROTE" -gt 0 ]
}
# Valori distinti letti per un attributo, spazio-separati.
attr_values() {
for _p in $(policies); do
readf "$_p/$1"
done | sort -u | tr '\n' ' ' | sed 's/ *$//'
}
p0=$(policies | head -n 1)
[ -n "$p0" ] || fail "nessuna policy cpufreq in $CPUFREQ"
driver=$(readf "$p0/scaling_driver" || echo unknown)
hw_min=$(readf "$p0/cpuinfo_min_freq" || echo 0)
hw_max=$(cf_hw_max_khz "$p0") # indipendente dallo stato del boost
floor=$(readf "$p0/amd_pstate_lowest_nonlinear_freq" || echo "")
[ -n "$floor" ] && [ "$floor" -gt "$hw_min" ] || floor=$hw_min
boost_global=$CPUFREQ/boost
[ -w "$boost_global" ] || [ -r "$boost_global" ] || boost_global=""
boost_read() {
for _p in $(policies); do
readf "$_p/boost" && return 0
done
[ -n "$boost_global" ] && readf "$boost_global" && return 0
readf "$INTEL_PSTATE/no_turbo" >/dev/null 2>&1 || return 1
_nt=$(readf "$INTEL_PSTATE/no_turbo") || return 1
[ "$_nt" = "1" ] && echo 0 || echo 1
}
print_state() {
printf 'driver=%s\n' "$driver"
printf 'mode=%s\n' "$(readf /sys/devices/system/cpu/amd_pstate/status || echo n/a)"
printf 'governor=%s\n' "$(attr_values scaling_governor)"
printf 'epp=%s\n' "$(attr_values energy_performance_preference)"
printf 'min_khz=%s\n' "$(readf "$p0/scaling_min_freq")"
printf 'max_khz=%s\n' "$(readf "$p0/scaling_max_freq")"
printf 'hw_min_khz=%s\n' "$hw_min"
printf 'hw_max_khz=%s\n' "$hw_max"
printf 'floor_khz=%s\n' "$floor"
printf 'boost=%s\n' "$(boost_read || echo n/a)"
printf 'policies=%s\n' "$(policies | wc -l | tr -d ' ')"
printf 'baseline=%s\n' "$([ -r "$BASELINE" ] && echo present || echo absent)"
printf 'refcount=%s\n' "$([ -r "$REFCOUNT" ] && readf "$REFCOUNT" || echo 0)"
}
set_governor() {
_g=${1:-}
[ -n "$_g" ] || fail "governor mancante"
case "$_g" in *[!a-z_-]*) fail "governor non valido: $_g" ;; esac
_avail=$(readf "$p0/scaling_available_governors") || fail "governor non leggibili"
case " $_avail " in
*" $_g "*) ;;
*) fail "governor '$_g' non disponibile ($_avail)" ;;
esac
write_all scaling_governor "$_g" || fail "scrittura scaling_governor fallita su $FAILED policy"
_now=$(attr_values scaling_governor)
[ "$_now" = "$_g" ] || fail "verifica governor fallita: letto '$_now'"
printf 'governor=%s\n' "$_g"
}
set_epp() {
_e=${1:-}
[ -n "$_e" ] || fail "preferenza EPP mancante"
case "$_e" in *[!a-z_]*) fail "preferenza EPP non valida: $_e" ;; esac
_avail=$(readf "$p0/energy_performance_available_preferences") \
|| fail "questo driver non espone energy_performance_preference"
case " $_avail " in
*" $_e "*) ;;
*) fail "EPP '$_e' non disponibile ($_avail)" ;;
esac
if write_all energy_performance_preference "$_e"; then
printf 'epp=%s\n' "$_e"
return 0
fi
# Con governor "performance" il driver EPP può ignorare la preferenza: non è
# un errore fatale, viene segnalato come warning.
if [ "$(attr_values scaling_governor)" = "performance" ]; then
printf 'warning=EPP non applicato con governor performance\n'
return 0
fi
fail "scrittura EPP fallita su $FAILED policy"
}
set_boost() {
_b=${1:-}
case "$_b" in 0|1) ;; *) fail "boost deve essere 0 o 1" ;; esac
_count=0
for _p in $(policies); do
if [ -w "$_p/boost" ] && write_one "$_p/boost" "$_b"; then
_count=$((_count + 1))
fi
done
if [ -n "$boost_global" ] && write_one "$boost_global" "$_b"; then
_count=$((_count + 1))
fi
if [ -w "$INTEL_PSTATE/no_turbo" ] && write_one "$INTEL_PSTATE/no_turbo" $((1 - _b)); then
_count=$((_count + 1))
fi
[ "$_count" -gt 0 ] || fail "nessun controllo boost scrivibile su questo sistema"
_now=$(boost_read || echo "")
[ "$_now" = "$_b" ] || printf 'warning=lettura boost dopo la scrittura: %s\n' "${_now:-n/d}"
printf 'boost=%s\n' "$_b"
}
set_limits() {
_min=${1:-}
_max=${2:-}
case "$_min$_max" in *[!0-9]*) fail "limiti non numerici (kHz interi)" ;; esac
[ -n "$_min" ] && [ -n "$_max" ] || fail "servono min_khz e max_khz"
[ "$_min" -ge "$hw_min" ] || fail "minimo $_min sotto il limite hardware $hw_min"
[ "$_max" -le "$hw_max" ] || fail "massimo $_max sopra il limite hardware $hw_max"
[ "$_min" -le "$_max" ] || fail "minimo $_min maggiore del massimo $_max"
_cur_min=$(readf "$p0/scaling_min_freq" || echo "$hw_min")
_cur_max=$(readf "$p0/scaling_max_freq" || echo "$hw_max")
# Ordine di scrittura sicuro: mai uno stato intermedio con min > max.
if [ "$_min" -gt "$_cur_max" ]; then
write_all scaling_max_freq "$_max" || fail "pre-allargamento del tetto fallito"
fi
if [ "$_max" -lt "$_cur_min" ]; then
write_all scaling_min_freq "$_min" || fail "pre-abbassamento del minimo fallito"
fi
write_all scaling_min_freq "$_min" || fail "scrittura scaling_min_freq fallita su $FAILED policy"
write_all scaling_max_freq "$_max" || fail "scrittura scaling_max_freq fallita su $FAILED policy"
printf 'min_khz=%s\n' "$(readf "$p0/scaling_min_freq")"
printf 'max_khz=%s\n' "$(readf "$p0/scaling_max_freq")"
}
apply_profile() {
_pr=${1:-}
cf_profile_known "$_pr" || fail "profilo '$_pr' sconosciuto ($(cf_profiles | tr '\n' ' '))"
_gov=$(cf_profile_gov "$_pr")
_epp=$(cf_profile_epp "$_pr")
_bst=$(cf_profile_boost "$_pr")
_maxf=$(cf_profile_max_khz "$_pr" "$floor" "$hw_max")
[ "$_maxf" -gt "$hw_max" ] && _maxf=$hw_max
[ "$_maxf" -lt "$floor" ] && _maxf=$floor
printf 'profile=%s\n' "$_pr"
# Ordine: 1) boost, 2) governor, 3) limiti, 4) EPP.
# Il boost viene per primo perché con boost disattivato il kernel riduce il
# tetto nominale e una scrittura di scaling_max_freq oltre quel valore
# verrebbe rifiutata o limitata.
set_boost "$_bst" || return 1
if readf "$p0/scaling_available_governors" | grep -qw -- "$_gov"; then
set_governor "$_gov" || return 1
else
printf 'warning=governor %s non disponibile, mantenuto %s\n' \
"$_gov" "$(attr_values scaling_governor)"
fi
set_limits "$floor" "$_maxf" || return 1
if readf "$p0/energy_performance_available_preferences" >/dev/null 2>&1; then
set_epp "$_epp" || return 1
fi
printf 'profile_label=%s\n' "$(cf_profile_label "$_pr")"
}
ensure_state_dir() {
if [ ! -d "$STATE_DIR" ]; then
mkdir -p "$STATE_DIR" || fail "impossibile creare $STATE_DIR"
chmod 755 "$STATE_DIR"
fi
}
save_state() {
ensure_state_dir
{
printf 'governor=%s\n' "$(readf "$p0/scaling_governor")"
printf 'epp=%s\n' "$(readf "$p0/energy_performance_preference" || echo "")"
printf 'min_khz=%s\n' "$(readf "$p0/scaling_min_freq")"
printf 'max_khz=%s\n' "$(readf "$p0/scaling_max_freq")"
printf 'boost=%s\n' "$(boost_read || echo "")"
printf 'saved_at=%s\n' "$(date +%s)"
} > "$BASELINE" || fail "scrittura baseline fallita"
chmod 644 "$BASELINE"
printf 'baseline=saved\n'
cat "$BASELINE"
}
restore_state() {
[ -r "$BASELINE" ] || fail "nessuna baseline salvata"
_gov=$(sed -n 's/^governor=//p' "$BASELINE")
_epp=$(sed -n 's/^epp=//p' "$BASELINE")
_min=$(sed -n 's/^min_khz=//p' "$BASELINE")
_max=$(sed -n 's/^max_khz=//p' "$BASELINE")
_bst=$(sed -n 's/^boost=//p' "$BASELINE")
if [ -n "$_gov" ]; then
if readf "$p0/scaling_available_governors" | grep -qw -- "$_gov"; then
write_all scaling_governor "$_gov" || printf 'warning=governor non ripristinato\n'
fi
fi
if [ -n "$_min" ] && [ -n "$_max" ]; then
case "$_min$_max" in
*[!0-9]*) ;;
*) set_limits "$_min" "$_max" || printf 'warning=limiti non ripristinati\n' ;;
esac
fi
if [ -n "$_epp" ] && readf "$p0/energy_performance_available_preferences" >/dev/null 2>&1; then
write_all energy_performance_preference "$_epp" || printf 'warning=EPP non ripristinato\n'
fi
if [ -n "$_bst" ]; then
case "$_bst" in
0|1) set_boost "$_bst" >/dev/null || printf 'warning=boost non ripristinato\n' ;;
esac
fi
printf 'restored=1\n'
print_state
}
reset_cmd() {
_gov=powersave
if ! readf "$p0/scaling_available_governors" | grep -qw -- "$_gov"; then
_gov=$(readf "$p0/scaling_available_governors" | awk '{print $1}')
fi
printf 'profile=reset\n'
[ -n "$_gov" ] && set_governor "$_gov"
set_limits "$floor" "$hw_max" || return 1
if readf "$p0/energy_performance_available_preferences" >/dev/null 2>&1; then
_epp=balance_performance
if ! readf "$p0/energy_performance_available_preferences" | grep -qw -- "$_epp"; then
_epp=default
fi
set_epp "$_epp" || return 1
fi
set_boost 1 || return 1
}
task_begin() {
_pr=${1:-}
cf_profile_known "$_pr" || fail "profilo '$_pr' sconosciuto"
ensure_state_dir
_rc=0
[ -r "$REFCOUNT" ] && _rc=$(readf "$REFCOUNT")
case "${_rc:-0}" in *[!0-9]*|"") _rc=0 ;; esac
if [ "$_rc" -eq 0 ] || [ ! -r "$BASELINE" ]; then
save_state >/dev/null || fail "salvataggio dello stato corrente fallito"
fi
apply_profile "$_pr" || fail "applicazione del profilo '$_pr' fallita"
_rc=$((_rc + 1))
printf '%s\n' "$_rc" > "$REFCOUNT"
chmod 644 "$REFCOUNT"
printf 'refcount=%s\n' "$_rc"
}
task_end() {
ensure_state_dir
_rc=0
[ -r "$REFCOUNT" ] && _rc=$(readf "$REFCOUNT")
case "${_rc:-0}" in *[!0-9]*|"") _rc=0 ;; esac
if [ "${1:-}" = "--all" ]; then
_rc=0
else
_rc=$((_rc - 1))
[ "$_rc" -lt 0 ] && _rc=0
fi
printf '%s\n' "$_rc" > "$REFCOUNT"
chmod 644 "$REFCOUNT"
if [ "$_rc" -eq 0 ] && [ -r "$BASELINE" ]; then
restore_state >/dev/null || printf 'warning=ripristino della baseline fallito\n'
rm -f "$BASELINE"
printf 'restored=1\n'
fi
printf 'refcount=%s\n' "$_rc"
}
case "$CMD" in
status)
print_state
;;
usage|-h|--help|help)
usage
;;
set-governor)
[ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec"
set_governor "${1:-}"
;;
set-epp)
[ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec"
set_epp "${1:-}"
;;
set-limits)
[ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec"
set_limits "${1:-}" "${2:-}"
;;
set-boost)
[ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec"
set_boost "${1:-}"
;;
apply)
[ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec"
apply_profile "${1:-}"
;;
reset)
[ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec"
reset_cmd
;;
save-state)
[ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec"
save_state
;;
restore-state)
[ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec"
restore_state
;;
task-begin)
[ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec"
task_begin "${1:-}"
;;
task-end)
[ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec"
task_end "${1:-}"
;;
*)
printf 'ok=0\nerror=comando sconosciuto: %s\n' "$CMD"
usage
exit 2
;;
esac
printf 'ok=1\n'
exit 0
+175
View File
@@ -0,0 +1,175 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# enne2-cpufreq — lettore di stato NON privilegiato per il plasmoide.
# Legge solo attributi world-readable di sysfs, /proc e hwmon; non scrive nulla.
# Uscita: righe key=value (formato usato dagli altri widget org.enne2.*).
set -u
SELF_DIR=$(dirname "$0")
if [ -r "$SELF_DIR/profiles.sh" ]; then
# shellcheck source=profiles.sh
. "$SELF_DIR/profiles.sh"
else
printf 'helper=missing\nerror=profiles.sh mancante\n'
exit 2
fi
CPUFREQ=/sys/devices/system/cpu/cpufreq
HELPER=/usr/local/libexec/enne2-cpufreq/cpufreq-helper
POLICY_POLKIT=/usr/share/polkit-1/actions/org.enne2.cpufreq.set.policy
RULE_POLKIT=/etc/polkit-1/rules.d/50-org.enne2.cpufreq.rules
CONF_DIR=${XDG_CONFIG_HOME:-$HOME/.config}/enne2-cpufreq
TASKS=$CONF_DIR/tasks.json
RUN=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/enne2-cpufreq
readf() {
[ -r "$1" ] || return 1
_val=
IFS= read -r _val < "$1" 2>/dev/null || [ -n "$_val" ] || return 1
printf '%s\n' "$_val"
}
policies() {
for _p in "$CPUFREQ"/policy[0-9]*; do
[ -d "$_p" ] && printf '%s\n' "$_p"
done
}
attr_values() {
for _p in $(policies); do
readf "$_p/$1"
done | sort -u | tr '\n' ' ' | sed 's/ *$//'
}
p0=$(policies | head -n 1)
if [ -z "$p0" ]; then
printf 'helper=missing\nerror=nessuna policy cpufreq (driver non cpufreq?)\n'
exit 2
fi
driver=$(readf "$p0/scaling_driver" || echo unknown)
hw_min=$(readf "$p0/cpuinfo_min_freq" || echo 0)
hw_max=$(cf_hw_max_khz "$p0") # indipendente dallo stato del boost
floor=$(readf "$p0/amd_pstate_lowest_nonlinear_freq" || echo "")
[ -n "$floor" ] && [ "$floor" -gt "$hw_min" ] || floor=$hw_min
min_khz=$(readf "$p0/scaling_min_freq" || echo 0)
max_khz=$(readf "$p0/scaling_max_freq" || echo 0)
governor=$(attr_values scaling_governor)
governors=$(readf "$p0/scaling_available_governors" || echo "")
epp=$(attr_values energy_performance_preference)
epps=$(readf "$p0/energy_performance_available_preferences" || echo "")
epp_supported=0
[ -n "$epps" ] && epp_supported=1
# boost: per-policy (kernel recenti) → globale → intel_pstate/no_turbo invertito
boost=$(readf "$p0/boost" || readf "$CPUFREQ/boost" || echo "")
boost_supported=0
[ -n "$boost" ] && boost_supported=1
if [ "$boost_supported" -eq 0 ] && [ -r /sys/devices/system/cpu/intel_pstate/no_turbo ]; then
_nt=$(readf /sys/devices/system/cpu/intel_pstate/no_turbo)
[ "$_nt" = "1" ] && boost=0 || boost=1
boost_supported=1
fi
# frequenza istantanea: massima fra le policy (core più attivo) e media
sum=0
cnt=0
curmax=0
for _p in $(policies); do
_f=$(readf "$_p/scaling_cur_freq") || continue
case "$_f" in ''|*[!0-9]*) continue ;; esac
sum=$((sum + _f))
cnt=$((cnt + 1))
[ "$_f" -gt "$curmax" ] && curmax=$_f
done
[ "$cnt" -gt 0 ] || cnt=1
cur_avg=$((sum / cnt))
temp=$(readf /sys/class/hwmon/hwmon0/temp1_input 2>/dev/null || echo "")
for _h in /sys/class/hwmon/hwmon*; do
[ -r "$_h/name" ] || continue
case "$(readf "$_h/name")" in
k10temp|coretemp|zenpower)
temp=$(readf "$_h/temp1_input" || echo "")
break
;;
esac
done
model=$(sed -n 's/^model name[[:space:]]*: //p' /proc/cpuinfo | head -n 1)
load1=$(awk '{print $1}' /proc/loadavg)
helper_state=missing
[ -x "$HELPER" ] && helper_state=ok
polkit_state=missing
if [ -r "$POLICY_POLKIT" ] && [ -r "$RULE_POLKIT" ]; then
polkit_state=ok
elif [ -r "$POLICY_POLKIT" ]; then
polkit_state=ask
fi
conflicts=""
for _svc in tuned power-profiles-daemon tlp; do
if systemctl is-active --quiet "$_svc" 2>/dev/null; then
conflicts="$conflicts $_svc"
fi
done
conflicts=${conflicts# }
profile=$(cf_detect_profile "${governor:-}" "${epp:-}" "${boost:-0}" "$max_khz" "$floor" "$hw_max")
printf 'driver=%s\n' "$driver"
printf 'mode=%s\n' "$(readf /sys/devices/system/cpu/amd_pstate/status || echo n/a)"
printf 'model=%s\n' "$model"
printf 'policies=%s\n' "$cnt"
printf 'governor=%s\n' "$governor"
printf 'governors=%s\n' "$governors"
printf 'epp=%s\n' "$epp"
printf 'epps=%s\n' "$epps"
printf 'epp_supported=%s\n' "$epp_supported"
printf 'boost=%s\n' "$boost"
printf 'boost_supported=%s\n' "$boost_supported"
printf 'hw_min_khz=%s\n' "$hw_min"
printf 'hw_max_khz=%s\n' "$hw_max"
printf 'floor_khz=%s\n' "$floor"
printf 'min_khz=%s\n' "$min_khz"
printf 'max_khz=%s\n' "$max_khz"
printf 'cur_khz=%s\n' "$curmax"
printf 'cur_avg_khz=%s\n' "$cur_avg"
printf 'temp_mdegc=%s\n' "$temp"
printf 'load1=%s\n' "$load1"
printf 'profile=%s\n' "$profile"
printf 'profile_label=%s\n' "$(cf_profile_label "$profile")"
printf 'helper=%s\n' "$helper_state"
printf 'polkit=%s\n' "$polkit_state"
printf 'conflicts=%s\n' "$conflicts"
[ -r /var/lib/enne2-cpufreq/baseline ] && printf 'baseline=present\n' || printf 'baseline=absent\n'
# ---- task configurati (un file JSON gestito da enne2-cpufreq-task) ----------
# Formato di ogni riga: task=<indice> <TAB> <running 0|1> <TAB> <oggetto JSON>
# Il JSON (jq -c, una riga) evita ogni ambiguità su spazi, tabulazioni e pipe
# dentro i comandi definiti dall'utente.
if command -v jq >/dev/null 2>&1 && [ -r "$TASKS" ]; then
_count=$(jq -r '.tasks | length' "$TASKS" 2>/dev/null || echo 0)
printf 'task_count=%s\n' "${_count:-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 // ""' 2>/dev/null)
_running=0
if [ -n "$_slug" ] && [ -r "$RUN/$_slug.pid" ]; then
_pid=$(readf "$RUN/$_slug.pid")
case "$_pid" in
''|*[!0-9]*) _running=0 ;;
*) kill -0 "$_pid" 2>/dev/null && _running=1 ;;
esac
fi
printf 'task=%s\t%s\t%s\n' "$_i" "$_running" "$_line"
_i=$((_i + 1))
done
else
printf 'task_count=0\n'
fi
printf 'ts=%s\n' "$(date +%s)"
+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
+145
View File
@@ -0,0 +1,145 @@
# shellcheck shell=sh
# Profili di frequenza condivisi fra helper privilegiato, lettore di stato e plasmoide.
# Sorgente unica di verità: NON scrive nulla su sysfs, contiene solo la tabella dei
# profili e l'aritmetica dei limiti (kHz). Copiato da install.sh accanto all'helper
# in /usr/local/libexec/enne2-cpufreq/.
#
# Ogni profilo agisce su: governor, energy_performance_preference (EPP), boost e
# tetto di frequenza espresso come percentuale dello span utile
# (floor_khz .. cpuinfo_max_freq).
# Lettura minimale (il file è già stato validato dal chiamante dove serve).
cf_read() {
_cfout=
[ -r "$1" ] || return 1
IFS= read -r _cfout < "$1" 2>/dev/null || [ -n "$_cfout" ] || return 1
printf '%s\n' "$_cfout"
}
# Tetto hardware reale (kHz), indipendente dallo stato del boost:
# con boost disattivato il kernel riporta in cpuinfo_max_freq la frequenza
# nominale (es. 3.80 GHz invece di 4.97 GHz su amd-pstate). amd_pstate_max_freq
# conserva il massimo effettivo; su altri driver cpuinfo_max_freq è già corretto.
cf_hw_max_khz() {
_cfp0=$1
_cfmax=$(cf_read "$_cfp0/cpuinfo_max_freq" || echo 0)
case "$_cfmax" in ''|*[!0-9]*) _cfmax=0 ;; esac
_cfamd=$(cf_read "$_cfp0/amd_pstate_max_freq" || echo 0)
case "$_cfamd" in ''|*[!0-9]*) _cfamd=0 ;; esac
[ "$_cfamd" -gt "$_cfmax" ] && _cfmax=$_cfamd
printf '%s\n' "$_cfmax"
}
cf_profiles() {
printf '%s\n' quiet powersave balanced performance
}
cf_profile_known() {
case "$1" in
quiet|powersave|balanced|performance) return 0 ;;
*) return 1 ;;
esac
}
cf_profile_gov() {
case "$1" in
quiet|powersave|balanced) echo powersave ;;
performance) echo performance ;;
*) return 1 ;;
esac
}
cf_profile_epp() {
case "$1" in
quiet) echo power ;;
powersave) echo balance_power ;;
balanced) echo balance_performance ;;
performance) echo performance ;;
*) return 1 ;;
esac
}
cf_profile_boost() {
case "$1" in
quiet|powersave) echo 0 ;;
balanced|performance) echo 1 ;;
*) return 1 ;;
esac
}
cf_profile_max_pct() {
case "$1" in
quiet) echo 35 ;;
powersave) echo 60 ;;
balanced|performance) echo 100 ;;
*) return 1 ;;
esac
}
cf_profile_label() {
case "$1" in
quiet) echo "Silenzioso" ;;
powersave) echo "Risparmio" ;;
balanced) echo "Bilanciato" ;;
performance) echo "Performance" ;;
custom) echo "Personalizzato" ;;
*) echo "$1" ;;
esac
}
cf_profile_desc() {
case "$1" in
quiet) echo "EPP power, boost off, tetto 35% dello span" ;;
powersave) echo "EPP balance_power, boost off, tetto 60%" ;;
balanced) echo "EPP balance_performance, boost on, nessun tetto" ;;
performance) echo "governor performance, EPP performance, boost on" ;;
*) echo "" ;;
esac
}
# cf_profile_max_khz <profilo> <floor_khz> <hw_max_khz>
cf_profile_max_khz() {
_cfp=$(cf_profile_max_pct "$1") || return 1
_cfl=$2
_cfh=$3
[ "$_cfh" -gt "$_cfl" ] || { echo "$_cfh"; return 0; }
# un profilo al 100% non deve avere tetto: nessun arrotondamento
[ "$_cfp" -ge 100 ] && { echo "$_cfh"; return 0; }
cf_round50 $(( _cfl + (_cfh - _cfl) * _cfp / 100 ))
}
# Arrotonda a 50 MHz: valori leggibili e stabili fra una lettura e l'altra.
cf_round50() {
echo $(( ($1 + 25000) / 50000 * 50000 ))
}
# cf_detect_profile <governor> <epp> <boost> <max_khz> <floor_khz> <hw_max_khz>
# Stampa il nome del profilo se lo stato coincide (tolleranza sul tetto) oppure "custom".
cf_detect_profile() {
_dg=$1
_de=$2
_db=$3
_dmax=$4
_dfloor=$5
_dhwmax=$6
_span=$(( _dhwmax - _dfloor ))
[ "$_span" -gt 0 ] || _span=1
_tol=$(( _span / 40 )) # 2.5% dello span
[ "$_tol" -lt 80000 ] && _tol=80000
for _dp in $(cf_profiles); do
[ "$_dg" = "$(cf_profile_gov "$_dp")" ] || continue
[ "$_db" = "$(cf_profile_boost "$_dp")" ] || continue
_dp_max=$(cf_profile_max_khz "$_dp" "$_dfloor" "$_dhwmax")
_diff=$(( _dmax - _dp_max ))
[ "$_diff" -lt 0 ] && _diff=$(( -_diff ))
[ "$_diff" -le "$_tol" ] || continue
# L'EPP può non essere scrivibile con governor "performance": in quel caso
# accettiamo il profilo anche se l'EPP letto non coincide.
[ "$_de" = "$(cf_profile_epp "$_dp")" ] && { echo "$_dp"; return 0; }
if [ "$_dg" = "performance" ] && [ "$_dp" = "performance" ]; then
echo "$_dp"
return 0
fi
done
echo custom
}
+55
View File
@@ -0,0 +1,55 @@
/*
* SPDX-FileCopyrightText: 2026 enne2
* SPDX-License-Identifier: MIT
*
* Pillola selezionabile usata per governor ed EPP: alternativa compatta a una
* ComboBox, senza stato interno (attivo/inattivo derivano dalle proprietà).
*/
import QtQuick
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.kirigami as Kirigami
Rectangle {
id: pill
property string text: ""
property bool active: false
property color accent: "#55c5c0"
signal clicked()
implicitWidth: label.implicitWidth + 16
implicitHeight: label.implicitHeight + 8
radius: 4
color: active
? Qt.rgba(accent.r, accent.g, accent.b, 0.22)
: (pillMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.10) : Qt.rgba(1, 1, 1, 0.05))
border.width: 1
border.color: active
? Qt.rgba(accent.r, accent.g, accent.b, 0.8)
: Qt.rgba(1, 1, 1, 0.10)
opacity: enabled ? 1.0 : 0.5
Behavior on color {
ColorAnimation { duration: 120 }
}
PlasmaComponents.Label {
id: label
anchors.centerIn: parent
text: pill.text
font.family: "monospace"
font.pointSize: 8
font.bold: pill.active
color: pill.active ? pill.accent : Kirigami.Theme.textColor
opacity: pill.active ? 1.0 : 0.78
}
MouseArea {
id: pillMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: pill.clicked()
}
}
+1139
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Executable
+147
View File
@@ -0,0 +1,147 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# enne2-cpufreq — installazione.
#
# ./install.sh widget (utente) + backend privilegiato (sudo)
# ./install.sh --widget solo il plasmoide, come utente
# ./install.sh --backend solo helper + polkit, come root
# ./install.sh --no-polkit-rule
# backend senza la regola "nessun prompt":
# ogni modifica chiederà la password
# ./install.sh --dry-run mostra cosa verrebbe fatto
set -u
PKG_DIR=$(cd "$(dirname "$0")" && pwd)
ID=org.enne2.cpufreq
LIBDIR=/usr/local/libexec/enne2-cpufreq
POLICY_DST=/usr/share/polkit-1/actions/org.enne2.cpufreq.set.policy
RULE_DST=/etc/polkit-1/rules.d/50-org.enne2.cpufreq.rules
STATEDIR=/var/lib/enne2-cpufreq
MODE=all
WITH_RULE=1
DRY=0
for arg in "$@"; do
case "$arg" in
--widget) MODE=widget ;;
--backend) MODE=backend ;;
--all) MODE=all ;;
--no-polkit-rule) WITH_RULE=0 ;;
--dry-run|-n) DRY=1 ;;
-h|--help)
sed -n 's/^# \{0,1\}//p' "$0" | sed -n '1,20p'
exit 0
;;
*) printf 'opzione sconosciuta: %s\n' "$arg" >&2; exit 2 ;;
esac
done
say() { printf '%s\n' "$*"; }
run() {
if [ "$DRY" -eq 1 ]; then
printf ' [dry-run] %s\n' "$*"
else
"$@"
fi
}
have_widget() {
kpackagetool6 --type Plasma/Applet --show "$ID" >/dev/null 2>&1
}
install_widget() {
command -v kpackagetool6 >/dev/null 2>&1 || {
say "ERRORE: kpackagetool6 non trovato (pacchetto kpackagetool6/kf6-kpackage)."
return 1
}
if [ "$DRY" -eq 1 ]; then
say "[dry-run] kpackagetool6 --type Plasma/Applet --install|--upgrade $PKG_DIR"
elif have_widget; then
say "Aggiorno il plasmoide $ID…"
kpackagetool6 --type Plasma/Applet --upgrade "$PKG_DIR" || return 1
else
say "Installo il plasmoide $ID…"
kpackagetool6 --type Plasma/Applet --install "$PKG_DIR" || return 1
fi
WIDGET_DIR=$HOME/.local/share/plasma/plasmoids/$ID
if [ "$DRY" -eq 0 ] && [ -d "$WIDGET_DIR/contents/scripts" ]; then
chmod 0755 "$WIDGET_DIR"/contents/scripts/*.sh \
"$WIDGET_DIR"/contents/scripts/enne2-cpufreq-task 2>/dev/null
fi
# comodità da terminale: stesso launcher del widget
if [ "$DRY" -eq 0 ] && [ -d "$HOME/.local/bin" ]; then
ln -sfn "$WIDGET_DIR/contents/scripts/enne2-cpufreq-task" \
"$HOME/.local/bin/enne2-cpufreq-task"
say "Comando utente: ~/.local/bin/enne2-cpufreq-task (list|add|run|stop)"
fi
return 0
}
install_backend() {
if [ "$(id -u)" -ne 0 ]; then
say "ERRORE: la parte privilegiata richiede root (usare sudo)."
return 1
fi
say "Backend privilegiato in $LIBDIR"
run install -d -m 0755 -o root -g root "$LIBDIR"
run install -d -m 0755 -o root -g root "$STATEDIR"
run install -m 0755 -o root -g root "$PKG_DIR/bin/cpufreq-helper" "$LIBDIR/cpufreq-helper"
run install -m 0644 -o root -g root "$PKG_DIR/contents/scripts/profiles.sh" "$LIBDIR/profiles.sh"
say "Azione polkit: $POLICY_DST"
run install -d -m 0755 /usr/share/polkit-1/actions
run install -m 0644 -o root -g root \
"$PKG_DIR/polkit/org.enne2.cpufreq.set.policy" "$POLICY_DST"
if [ "$WITH_RULE" -eq 1 ]; then
say "Regola polkit senza prompt (gruppo wheel, sessione attiva): $RULE_DST"
run install -d -m 0755 /etc/polkit-1/rules.d
run install -m 0644 -o root -g root \
"$PKG_DIR/polkit/50-org.enne2.cpufreq.rules" "$RULE_DST"
else
say "Regola senza prompt NON installata: verrà chiesta la password a ogni modifica."
run rm -f "$RULE_DST"
fi
# polkitd sorveglia le directory delle regole, il reload è solo una sicurezza
if [ "$DRY" -eq 0 ]; then
systemctl try-reload-or-restart polkit >/dev/null 2>&1 || true
fi
say ""
say "Verifica:"
if [ "$DRY" -eq 0 ]; then
"$LIBDIR/cpufreq-helper" status | sed 's/^/ /'
fi
return 0
}
case "$MODE" in
widget)
install_widget
;;
backend)
install_backend
;;
all)
install_widget || exit 1
say ""
if [ "$(id -u)" -eq 0 ]; then
install_backend || exit 1
else
say "Parte privilegiata: sudo $0 --backend $([ "$WITH_RULE" -eq 0 ] && echo --no-polkit-rule)"
if [ "$DRY" -eq 1 ]; then
sudo -n true >/dev/null 2>&1 && sudo "$0" --backend $([ "$WITH_RULE" -eq 0 ] && echo --no-polkit-rule) || say "[dry-run] sudo richiesto"
else
sudo "$0" --backend $([ "$WITH_RULE" -eq 0 ] && echo --no-polkit-rule) || exit 1
fi
fi
;;
esac
say ""
say "Fatto. Aggiungi il widget dal desktop (Aggiungi widget → «CPU Frequency Control»)"
say "oppure via script: desktops()[0].addWidget(\"$ID\")"
+19
View File
@@ -0,0 +1,19 @@
{
"KPackageStructure": "Plasma/Applet",
"KPlugin": {
"Id": "org.enne2.cpufreq",
"Name": "CPU Frequency Control",
"Description": "Controlla governor, limiti di frequenza (min/max), EPP e boost della CPU, applica profili di risparmio o performance e lancia task con un profilo dedicato, ripristinando lo stato precedente alla fine. Backend: helper privilegiato via pkexec + azione polkit org.enne2.cpufreq.set.",
"Icon": "cpu",
"Version": "1.0",
"License": "MIT",
"Authors": [
{
"Name": "enne2"
}
],
"Category": "System Information",
"Website": "https://git.enne2.net/enne2/kde-cpufreq-dashboard"
},
"X-Plasma-API-Minimum-Version": "6.0"
}
+22
View File
@@ -0,0 +1,22 @@
/* SPDX-License-Identifier: MIT
*
* enne2-cpufreq — consenso senza prompt per l'helper del plasmoide.
*
* Il permesso riguarda UNA sola azione (org.enne2.cpufreq.set) e quindi un solo
* programma: /usr/local/libexec/enne2-cpufreq/cpufreq-helper, con gli argomenti
* validati al suo interno. Condizioni: utente locale, sessione attiva, gruppo
* wheel. Senza questa regola la .policy chiede la password a ogni modifica
* (allow_active=auth_admin_keep): basta eliminare questo file per tornare al
* comportamento con prompt.
*/
polkit.addRule(function (action, subject) {
if (action.id !== "org.enne2.cpufreq.set") {
return;
}
if (!subject.local || !subject.active) {
return;
}
if (subject.isInGroup("wheel")) {
return polkit.Result.YES;
}
});
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<!--
org.enne2.cpufreq.set — azione polkit dedicata all'helper privilegiato del
plasmoide CPU Frequency Control (org.enne2.cpufreq).
L'annotazione exec.path fa sì che pkexec usi QUESTA azione (e non la generica
org.freedesktop.policykit.exec) solo quando esegue l'helper indicato: nessun
altro programma eredita il permesso.
-->
<policyconfig>
<vendor>enne2</vendor>
<vendor_url>https://git.enne2.net/enne2/kde-cpufreq-dashboard</vendor_url>
<action id="org.enne2.cpufreq.set">
<description>Configurare governor, limiti di frequenza e boost della CPU</description>
<message>Per modificare governor, limiti di frequenza, EPP o boost della CPU è necessaria l'autorizzazione di amministratore.</message>
<icon_name>cpu</icon_name>
<defaults>
<allow_any>no</allow_any>
<allow_inactive>no</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">/usr/local/libexec/enne2-cpufreq/cpufreq-helper</annotate>
</action>
</policyconfig>
Executable
+64
View File
@@ -0,0 +1,64 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# enne2-cpufreq — disinstallazione.
# ./uninstall.sh rimuove widget (utente) e backend (sudo)
# ./uninstall.sh --widget solo il plasmoide
# ./uninstall.sh --backend solo helper/polkit/stato (root)
# I task configurati (~/.config/enne2-cpufreq) NON vengono cancellati.
set -u
PKG_DIR=$(cd "$(dirname "$0")" && pwd)
ID=org.enne2.cpufreq
LIBDIR=/usr/local/libexec/enne2-cpufreq
POLICY_DST=/usr/share/polkit-1/actions/org.enne2.cpufreq.set.policy
RULE_DST=/etc/polkit-1/rules.d/50-org.enne2.cpufreq.rules
STATEDIR=/var/lib/enne2-cpufreq
MODE=all
for arg in "$@"; do
case "$arg" in
--widget) MODE=widget ;;
--backend) MODE=backend ;;
--all) MODE=all ;;
-h|--help) sed -n 's/^# \{0,1\}//p' "$0" | sed -n '1,10p'; exit 0 ;;
*) printf 'opzione sconosciuta: %s\n' "$arg" >&2; exit 2 ;;
esac
done
remove_widget() {
if kpackagetool6 --type Plasma/Applet --show "$ID" >/dev/null 2>&1; then
printf 'Rimuovo il plasmoide %s…\n' "$ID"
kpackagetool6 --type Plasma/Applet --remove "$ID" || return 1
else
printf 'Plasmoide %s non installato.\n' "$ID"
fi
[ -L "$HOME/.local/bin/enne2-cpufreq-task" ] && rm -f "$HOME/.local/bin/enne2-cpufreq-task"
return 0
}
remove_backend() {
[ "$(id -u)" -eq 0 ] || { printf 'ERRORE: richiede root.\n' >&2; return 1; }
# ripristina lo stato hardware prima di togliere l'helper
if [ -x "$LIBDIR/cpufreq-helper" ]; then
"$LIBDIR/cpufreq-helper" task-end --all >/dev/null 2>&1
fi
rm -f "$POLICY_DST" "$RULE_DST"
rm -rf "$LIBDIR"
rm -rf "$STATEDIR"
systemctl try-reload-or-restart polkit >/dev/null 2>&1 || true
printf 'Backend rimosso.\n'
return 0
}
case "$MODE" in
widget) remove_widget ;;
backend) remove_backend ;;
all)
remove_widget
if [ "$(id -u)" -eq 0 ]; then
remove_backend
else
sudo "$0" --backend
fi
;;
esac