Skip to content

fix: PowerProfile — powerprofilesctl/tlpctl support, fallback, and race condition fixes#89

Open
saken78 wants to merge 1 commit intoAxenide:mainfrom
saken78:fix-power-profiles
Open

fix: PowerProfile — powerprofilesctl/tlpctl support, fallback, and race condition fixes#89
saken78 wants to merge 1 commit intoAxenide:mainfrom
saken78:fix-power-profiles

Conversation

@saken78
Copy link
Contributor

@saken78 saken78 commented Feb 26, 2026

PowerProfile Service — Changelog

Manages power profiles via powerprofilesctl (primary) or tlpctl (fallback).
Supports optimistic UI updates with automatic rollback on failure and stale-read protection.


Screenshot_2026-02-26-15-34-40

Execution Flow

Initial Load

Component.onCompleted
  └─ checkPowerProfilesCtl.running = true
       command: bash -c "command -v powerprofilesctl"
       │
       ├─ exit 0 → powerprofilesctl path
       │    backendType = "powerprofilesctl"
       │    isAvailable = true
       │    _initialLoad = true
       │    └─ getProc.running = true
       │         command: powerprofilesctl get
       │         onRead  → currentProfile = <value>
       │                   profileChanged(profile)
       │         onExited → _initialLoad is true
       │                    └─ listProc.running = true
       │                         command: bash -c "powerprofilesctl list 2>&1"
       │                         onExited → parse profiles
       │                                    availableProfiles = [power-saver, balanced, performance]
       │                                    (sorted, falls back to defaults if parse yields nothing)
       │
       └─ exit ≠ 0 → TLP fallback path
            checkTLP.running = true
            command: bash -c "command -v tlp"
            │
            ├─ exit 0 → TLP path
            │    backendType = "tlp"
            │    isAvailable = true
            │    availableProfiles = ["power-saver", "balanced", "performance"] (hardcoded)
            │    └─ getTLPProc.running = true
            │         command: /sbin/tlpctl get
            │         onRead  → parse output → currentProfile = <value>
            │                                  profileChanged(profile)
            │
            └─ exit ≠ 0 → isAvailable = false (no backend found)

Set Profile

setProfile("balanced")
  │
  ├─ guard: !isAvailable → warn + return
  ├─ guard: profile not in availableProfiles → warn + return
  ├─ guard: _isSettingProfile || setProc.running
  │    └─ _pendingProfile = "balanced" → return (queued)
  │
  └─ proceed:
       _isSettingProfile = true
       _expectedProfile  = "balanced"
       currentProfile    = "balanced"   ← optimistic UI update
       profileChanged("balanced")
       setProc.command   = ["powerprofilesctl", "set", "balanced"]
                         | ["tlpctl", "set", "balanced"]
       setProc.running   = true
            │
            ├─ exit 0 (success)
            │    _isSettingProfile = false
            │    _expectedProfile  = ""   ← reset so future reads are not filtered
            │    _pendingProfile != ""
            │      └─ setProfile(_pendingProfile)  ← process queue
            │    (no confirmation read — avoids race with backend apply latency)
            │
            └─ exit ≠ 0 (failure)
                 _isSettingProfile = false
                 _expectedProfile  = ""
                 _pendingProfile   = ""
                 Qt.callLater →
                   getProc.running = true    (powerprofilesctl)
                   getTLPProc.running = true (tlp)
                   └─ reads actual backend state → currentProfile rollback

Stale-Read Protection

Any read arriving in getProc.onRead or getTLPProc.onRead while
_isSettingProfile = true and the returned profile ≠ _expectedProfile
is silently discarded. This prevents a slow backend response from
overwriting the optimistic update with the old value.


test3

  • Fix: _expectedProfile is now reset to "" after a successful set.
    Previously the filter remained active indefinitely, causing subsequent
    valid reads (e.g. from updateCurrentProfile()) to be silently discarded
    if the profile happened to differ from the last set target.

  • Fix: checkTLP now uses only command -v tlp instead of
    command -v tlp && tlp --version. tlp --version requires root on
    several distributions and returns non-zero even when tlp is correctly
    installed, producing a false-negative that silently skipped the fallback.

  • Fix: getProc.onExited now triggers listProc only on initial load
    (guarded by _initialLoad flag). Previously every rollback read after a
    failed set would also re-run listProc, which was unnecessary and
    produced redundant process spawns.


test2

  • Fix: Removed the post-success confirmation read in setProc.onExited.
    The confirmation getTLPProc / getProc call was issued via Qt.callLater
    immediately after set, racing against the backend applying the change and
    consistently returning the stale pre-change value, which then overwrote the
    correct optimistic state.

  • Fix: Added _isSettingProfile + _pendingProfile guard in setProfile.
    setProc is now never re-triggered while still running. If a second set
    request arrives during an in-flight operation, it is stored in
    _pendingProfile and processed automatically in setProc.onExited.

  • Fix: Added _expectedProfile stale-read filter in getProc.onRead and
    getTLPProc.onRead. Reads arriving while a set is in-flight are compared
    against _expectedProfile and discarded if they do not match, preventing
    the backend's delayed response from corrupting the optimistic UI state.


test1

  • Fix: checkPowerProfilesCtl and checkTLP now use a bash -c "command -v …"
    wrapper instead of invoking the binary directly. Quickshell Process does not
    go through a shell; if the binary is absent the OS-level exec fails silently,
    onExited never fires, and the TLP fallback is never reached.

  • Fix: listProc is now triggered sequentially from getProc.onExited instead
    of via two independent Qt.callLater calls in checkPowerProfilesCtl.onExited.
    The original approach started both processes in parallel with no ordering
    guarantee, allowing listProc to overwrite availableProfiles mid-read.

  • Fix: listProc failure no longer resets backendType to "" and
    re-triggers checkTLP. Since powerprofilesctl was already confirmed present,
    the backend is kept and availableProfiles falls back to the known-safe defaults
    ["power-saver", "balanced", "performance"].

  • Fix: setProc.command is no longer mutated while the process may still be
    running. The previous implementation assigned a new command array and immediately
    set running = true without checking setProc.running, risking a command change
    mid-execution on rapid successive calls.

…(fallback).

Supports optimistic UI updates with automatic rollback on failure and stale-read protection.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant