Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .github/ISSUE_TEMPLATE/gui-not-updating.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
name: GUI not updating / daemon unreachable
about: Use this when the Archer GUI shows stale data, says "Daemon Offline" / "Stale", or won't open at all.
title: "[GUI] "
labels: [gui]
assignees: []
---

<!--
Please paste the output of every command in the template below. Even if
something looks irrelevant, leaving it out usually means we have to ask
for it in a follow-up. The faster we have all of this, the faster the
fix lands.
-->

## What you saw

<!-- Describe the symptom. A screenshot of the GUI window helps a lot. -->

## What you expected

<!-- e.g. "Dashboard should show live CPU/GPU temperatures." -->

## Environment

- **Distro and version**: <!-- e.g. CachyOS 2026.04, EndeavourOS 2026.04 -->
- **Kernel** (`uname -r`):
- **Archer commit**: <!-- run `git -C /path/to/Archer rev-parse --short HEAD` -->
- **Hardware**: <!-- e.g. Acer Nitro 5 AN515-58 -->

## Daemon status

<details>
<summary><code>systemctl status archer-daemon</code></summary>

```
PASTE OUTPUT HERE
```

</details>

<details>
<summary><code>journalctl -u archer-daemon -n 100 --no-pager</code></summary>

```
PASTE OUTPUT HERE
```

</details>

## D-Bus visibility

<details>
<summary><code>busctl list | grep -i archer</code></summary>

```
PASTE OUTPUT HERE
```

</details>

<details>
<summary><code>busctl introspect io.otectus.Archer1 /io/otectus/Archer1 2>&1 | head -50</code></summary>

```
PASTE OUTPUT HERE
```

</details>

## GUI logs

<details>
<summary><code>journalctl --user -t archer-gui -n 100 --no-pager</code></summary>

```
PASTE OUTPUT HERE (if empty, paste any error printed to the terminal when you ran `archer-gui`)
```

</details>

## Did you try

- [ ] `sudo systemctl reload dbus.service && sudo systemctl restart archer-daemon`
- [ ] Re-running the installer (`./install.sh --modules gui --no-confirm`)
- [ ] Reading [README → Troubleshooting](https://github.com/otectus/Archer#troubleshooting)
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,39 @@ jobs:
run: pip install flake8
- name: Lint Python files
run: flake8 gui/ --max-line-length=120 --ignore=E501,W503,E402

python-syntax:
name: Python Syntax Check (GUI)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Compile every GUI module
run: |
set -e
for f in $(find gui -name '*.py'); do
echo "py_compile $f"
python -m py_compile "$f"
done

dbus-introspect-smoke:
name: D-Bus Introspect Smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install D-Bus runtime
run: |
sudo apt-get update
sudo apt-get install -y dbus dbus-x11 python3-dbus python3-gi systemd
- name: Boot a session bus, register the service, introspect
run: |
# The service runs against a private session bus so CI does not
# need root or a real system bus. We only assert that the
# service registers, exposes Ping, GetMonitoringData, and the
# TelemetryUpdated/AudioEnhancementChanged signals.
dbus-run-session -- bash tests/dbus_smoke.sh
139 changes: 139 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Changelog

All notable changes to Archer Compatibility Suite are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.1] — 2026-05-01

Hardening sweep triggered by [#4](https://github.com/otectus/Archer/issues/4)
("Archer GUI not updating and some other problems"). Closes that issue and a
broad set of correctness/security bugs surfaced by audit.

GUI package version bumped in lockstep: `1.0.0` → `1.0.1`.

### Fixed

- **Installer no longer requires a reboot to connect.** `modules/gui.sh` now
reloads `dbus.service` (with `kill -HUP $(pidof dbus-daemon)` fallback) after
copying `io.otectus.Archer1.conf`, so the new policy takes effect immediately.
- **GUI no longer freezes when the daemon is slow.** Every D-Bus call from
`gui/archer/client.py` now passes a 5s `timeout=` (longer for `envycontrol`,
`fwupdmgr`, restart). Without it, dbus-python defaulted to no timeout and a
single hung sysfs read in the daemon would freeze the polling thread forever.
- **Status label now reflects reality.** The window's "Connected" label was set
once at startup and never updated. It now flips to "Daemon Offline" or
"Stale" with exponential-backoff reconnect (5/10/20/60s) and surfaces the
underlying error in a toast.
- **Audio noise-suppression toggle now actually takes effect.** The daemon's
`systemctl --user restart pipewire.service` ran inside the *root* user
manager, never touching the user's pipewire. Replaced with an
`AudioEnhancementChanged` D-Bus signal that the GUI handles in the user's
own session.
- **`restart_daemon` no longer "loses" the D-Bus reply.** The synchronous
`systemctl restart` killed the daemon before the reply flushed. Now uses
`systemd-run --on-active=2s --no-block` so the GUI sees a clean response.
- **`get_fan_rpm` now picks the right hwmon device.** Allowlists Acer-relevant
chipsets (`linuwu_sense`, `acer_wmi`, `nct67xx`, `it87`, `dell_smm_hwmon`)
before falling back to "first device with `fan1_input`".
- **Setter UI controls revert on daemon failure.** Battery limit switch, USB
charging combo, LCD override switch, boot animation switch, backlight
timeout switch now flip back to the previous value with an explanatory
toast when the daemon refuses (was fire-and-forget — UI lied about state).
- **Tray init failures are no longer silent.** Logged at WARNING and surfaced
in a toast on first window show so the close-to-tray hint isn't bogus.
- **`uninstall.sh` no longer rm-rfs `/.local/...` if `$HOME` is unset under
sudo.** Guard added.

### Changed

- **Telemetry now arrives via D-Bus signal (`TelemetryUpdated`) instead of
polling.** The daemon emits every 2s; the GUI subscribes and watches for
staleness. The previous polling loop spawned a new thread every 2s and
would silently pile up zombies if any single call hung. `GetMonitoringData`
remains for the initial settings fetch and backward compat.
- **D-Bus is now mandatory.** The Unix-socket fallback in
`gui/archer_daemon.py` (`DaemonServer` class, ~340 LOC) was unreachable
from the user-mode GUI anyway (socket was 0o660 root:root). Removed.
`gui/archer/client.py` similarly drops the socket-fallback path. A failed
D-Bus startup now logs an actionable error pointing at the policy file
and `dbus reload`.
- **Service unit hardened.** `gui/archer-daemon.service` now uses
`RuntimeDirectory=archer` (so systemd creates `/run/archer/` with
0755 root:root) and `ReadWritePaths=/etc/archer` (required by
`ProtectSystem=full`). PID file moved from `/var/run/archer-daemon.pid`
to `/run/archer/daemon.pid`. `Requires=dbus.service` added.
- **Install manifest moved to `/var/lib/archer/install-manifest.json`**
(root-owned, 0644). Resolves the install-as-user / uninstall-as-sudo
`$HOME` mismatch and stops local users tampering with manifest entries
that `uninstall.sh` later sources. Manifests at the previous user-home
paths (and the legacy DAMX path) are migrated automatically on the next
install or uninstall run.
- **Per-module install rollback.** `INSTALLED_FILES` / `INSTALLED_DKMS` /
`INSTALLED_PACKAGES` are snapshotted before each `module_install` and
restored on failure, so the saved manifest only lists modules that
actually installed cleanly.
- **Daemon hot-path probes are cached** (5s TTL). `nvidia-smi`, `lspci`,
and `which envycontrol` results are reused across the GUI's monitoring
ticks so a hung NVIDIA driver no longer drives the daemon thread into
the ground.

### Security

- **Path-traversal in `uninstall.sh` closed.** The uninstall manifest source
loop now refuses to `source` any module name that fails the
`^[a-z][a-z0-9_-]+$` allowlist *and* isn't in the canonical `MODULE_IDS`
array. A tampered `~/.local/share/archer/install-manifest.json` could
previously inject e.g. `mod="../../tmp/evil"` and gain code execution
under `sudo` when the user ran uninstall.
- **`run_cmd` shell-meta guard.** `gui/archer_daemon.py:run_cmd` now refuses
any command containing `;`, `&&`, `||`, `$(`, or backticks unless the
caller passes `shell_meta_ok=True`. No present callsite is exploitable;
the guard catches future regressions where a user-supplied value flows
into a shell string.
- **`MODULE_IDS` is now a single source of truth** in `lib/modules.sh`,
consumed by both `install.sh` (validating `--modules`) and `uninstall.sh`
(validating manifest entries before `source`).

### Added

- **`tests/dbus_smoke.py` + `tests/dbus_smoke.sh`.** Headless smoke harness
that boots `ArcherDBusService` against a private session bus with a mocked
`HardwareManager`, asserts the required methods + signals appear in the
introspection XML, and waits for the first `TelemetryUpdated` emit.
- **CI: `python-syntax` and `dbus-introspect-smoke` jobs.**
`python -m py_compile` for every `gui/**/*.py`; smoke harness via
`dbus-run-session` so CI fails loudly on signal/method regressions.
- **`.github/ISSUE_TEMPLATE/gui-not-updating.md`** with required fields:
distro+kernel, daemon status, journal, `busctl introspect`, GUI logs.
Would have closed #4 in one round trip.
- **README → Troubleshooting** section covering "Daemon Offline / Stale"
and "GUI hangs forever" with the exact triage commands.
- **`is_known_module` allowlist with ~25 lines of bats tests** covering
path-traversal, shell-metacharacter, and well-formed-but-unknown IDs.

### Removed

- **`gui/install-gui.sh`** legacy wrapper. Use `./install.sh --modules gui`.
- **`DaemonServer` class and `/var/run/archer.sock`** from the daemon.
- **Unix-socket fallback path** from `gui/archer/client.py`.

### Migration notes for upgraders from 2.0.0

- The first run of `./install.sh` after upgrading copies your existing
`~/.local/share/archer/install-manifest.json` (and any legacy
`~/.local/share/damx/install-manifest.json`) into `/var/lib/archer/`.
Nothing else needed.
- If the daemon is currently running with a `/var/run/archer-daemon.pid`
PID file, the new unit's `RuntimeDirectory=archer` will create
`/run/archer/` on next start. Old `/var/run/archer.sock` is removed by
the uninstall path; no manual cleanup required.
- A short `systemctl reload dbus.service` happens during the GUI module
install. On most desktops this is invisible; you may briefly see your DE's
panel/applets reconnect.

## [2.0.0] — 2026-04-20

Initial release with D-Bus IPC + polkit authorization, 13 installer modules,
GTK4/Adwaita control panel, install manifest, and CI/test scaffolding.
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,45 @@ Archer/
- **CachyOS**: The installer automatically detects CachyOS kernels and installs the correct `-cachyos-headers` package. Clang/LLVM compiler flags are applied when a Clang-built kernel is detected.
- **AUR Helpers**: Modules that install AUR packages (battery, GPU, audio-enhance) prefer `paru` or `yay` if available, with manual fallback otherwise. The installer never installs an AUR helper for you.
- **D-Bus / Polkit**: The daemon registers as `io.otectus.Archer1` on the system bus. Read-only methods are unprivileged. Mutating methods require polkit authorization, cached per session (`auth_admin_keep`). System-level operations (restart, modprobe) always prompt (`auth_admin`).
- **Install Manifest**: Stored at `~/.local/share/archer/install-manifest.json`. Tracks installed modules, files, DKMS modules, and packages for clean uninstallation. Legacy manifests from previous versions are migrated automatically.
- **Install Manifest**: Stored at `/var/lib/archer/install-manifest.json` (root-owned, 0644). Tracks installed modules, files, DKMS modules, and packages for clean uninstallation. Manifests from older user-home locations (`~/.local/share/archer/`, legacy `~/.local/share/damx/`) are migrated automatically on the next install or uninstall run.
- **Fan Curve Safety**: The fan curve engine includes a watchdog that restores EC automatic control if the daemon crashes or 3 consecutive control ticks fail.

## Troubleshooting

### GUI shows "Daemon Offline" or "Stale"

1. Confirm the daemon is running:
```bash
systemctl status archer-daemon
```
If it's not active, start it: `sudo systemctl restart archer-daemon`.

2. Confirm the D-Bus name is claimable:
```bash
busctl list | grep io.otectus.Archer1
busctl introspect io.otectus.Archer1 /io/otectus/Archer1
```
If the name isn't visible, the policy file may not be loaded. The installer reloads `dbus.service` automatically, but you can do it manually:
```bash
sudo systemctl reload dbus.service
sudo systemctl restart archer-daemon
```

3. Tail the daemon log for the actual failure:
```bash
sudo journalctl -u archer-daemon -n 100 --no-pager
```

If the GUI shows the status flipping between "Connected" and "Stale", the daemon is up but not emitting telemetry on schedule — check `journalctl` for `TelemetryUpdated emit failed` messages (often a transient sysfs/nvidia-smi hang).

### GUI hangs forever / never opens

Almost always means D-Bus is unreachable. Same triage as above. The GUI now applies a 5s timeout to every D-Bus call and shows the failure in a toast, so a daemon hang manifests as a quick "Daemon Offline" notification rather than a frozen window.

## Changelog

Release notes and migration guidance: [CHANGELOG.md](CHANGELOG.md).

## Contributing

To add a new module, create `modules/<id>.sh` implementing the module interface:
Expand Down
20 changes: 17 additions & 3 deletions gui/archer-daemon.service
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
[Unit]
Description=Archer Compatibility Suite - System Daemon
After=multi-user.target
After=multi-user.target dbus.service
Wants=multi-user.target
Requires=dbus.service

[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/archer/archer_daemon.py
Restart=on-failure
RestartSec=5
PIDFile=/var/run/archer-daemon.pid

# systemd creates /run/archer/ (0755 root:root) at every service start and
# removes it on stop, so daemon.pid lives in a known clean location.
RuntimeDirectory=archer
RuntimeDirectoryMode=0755
PIDFile=/run/archer/daemon.pid

# Security hardening
ProtectSystem=full
ProtectHome=read-only
NoNewPrivileges=false
PrivateTmp=true

# Daemon must persist user settings under /etc/archer/. ProtectSystem=full
# would otherwise make /etc read-only.
ReadWritePaths=/etc/archer

# Daemon writes to /sys (fan, RGB, thermal_profile) so we cannot enable
# ProtectKernelTunables=true. NoNewPrivileges stays false because polkit's
# CheckAuthorization model and any future helper exec require it.
NoNewPrivileges=false

[Install]
WantedBy=multi-user.target
2 changes: 1 addition & 1 deletion gui/archer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Archer Compatibility Suite - GUI Application"""

__version__ = "1.0.0"
__version__ = "1.0.1"
Loading
Loading