#!/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 → 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 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 _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 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 _name=${1:-} _prof=${2:-} [ "$#" -ge 3 ] || fail "servono nome, profilo e comando" shift 2 do_add "$_name" "$_prof" "$*" } cmd_add_hex() { # add-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 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 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 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 2>&1 & printf 'ok=1\nname=%s\nslug=%s\nprofile=%s\npid=%s\n' "$_name" "$_slug" "$_prof" "$_pid" } cmd_stop() { # stop 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 (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 [argomenti] list elenco dei task configurati add add-hex argomenti in esadecimale UTF-8 (usato dal widget) remove run salva lo stato, applica il profilo, esegue, ripristina stop 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