Ipc::subscribe() assumes the next message on the event socket is the
reply to the IPC_SUBSCRIBE it just sent.
When a module subscribes more than once (sway/workspaces subscribes to
"workspace" and then to "window"), an event from the first subscription
can arrive before the reply to the second one. The payload check fails
and the thrown exception permanently disables the module for that bar.
This is easy to hit when bars are (re)created on output hotplug, since
sway emits a burst of workspace events at exactly that moment while
moving workspaces to the new output:
[warning] module sway/workspaces: Disabling module "sway/workspaces",
Unable to subscribe ipc event
Fix it by skipping over event messages (type high bit set) until the
subscribe reply is found, re-emitting them on signal_event so none are
lost.
Fixes: #5218
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>
Cache ALabel tooltip markup and provide it through query-tooltip instead of repeatedly setting GtkWidget's tooltip-markup property. This prevents frequently updating modules from restarting GTK's display-wide tooltip timer.
Keep the active tooltip updated directly so dynamic tooltip contents continue refreshing while the pointer remains stationary.
When min-brightness is set, a scroll step that would cross it was not
clamped — only scrolling while already at or below min was blocked.
With scroll-step: 5 and min-brightness: 1, scrolling from 5% would
pass the guard (5 > 1) and set brightness to 0%.
Fix by computing the post-step value before applying it: if it would
fall below min-brightness, set exactly to min-brightness instead.
Synchronize the cava module man page with the current implementation:
- Fix scdoc table syntax (correct header/body cell prefixes)
- Add missing Waybar-side options: format-icons, vertex_shader, fragment_shader
- Remove options not consumed by the module (data_format, raw_target)
- Clarify option scope: raw-only, GLSL-only, and cava-config-only settings
- Update descriptions to match backend behavior:
- ascii_range is derived from format-icons length
- bar_height is ignored by Waybar
- bar_delimiter is used for raw output
- Polish grammar, tighten structure, and fix example filenames
- Refresh STYLE and FRONTENDS sections to match the actual source
hardware.sh called smoke::stop between umockdev cases, but smoke::stop runs
`swaymsg exit` and tears down the shared compositor -- so only the first case
(backlight) had a display and the rest failed with "cannot open display".
Kill only waybar between cases; leave compositor teardown to the EXIT trap.
The state tier started actually running mpd in 74f0d33, which exposed a
deadlock in teardown: state.sh launches `mpd --no-daemon &` and never kills
it during the tier, and its cleanup() called smoke::stop *before* killing mpd.
smoke::stop ended in a bare `wait`, which reaps *every* background job of the
shell -- including the still-running mpd -- so it blocked until GitHub's 6h
job timeout. continue-on-error doesn't help: it catches failures, not hangs.
- lib.sh: smoke::stop now waits only on the PIDs it owns (waybar, compositor),
so an unrelated daemon left running by a tier can't deadlock teardown.
- state.sh: cleanup() tears down mpd/pulseaudio before smoke::stop.
- smoke.yml: add timeout-minutes: 20 so a future hang fails fast instead of
burning the default 6h runner budget.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
onEvent() runs on the Hyprland IPC listener thread and mutated the label's
style context (add/remove class) directly, racing the GTK main thread's
drawing and corrupting the heap (double free / corrupted double-linked list).
Follow the Submap pattern: onEvent only stores the layout under the mutex and
emits the dispatcher; update() swaps the CSS class on the main thread, tracking
the previously applied class.
Fixes#4665
tooltip-format-enumerate-connected[-battery] only received the device_* args,
so {status}, {num_connections} and {controller_*} threw fmt "argument not
found". Pass those args to the enumerate fmt::format as well.
Fixes#4384
loadConfig() called exit(EXIT_FAILURE) when the cava config failed to load or
no input source was available, killing the whole bar. Throw std::runtime_error
instead: the factory/bar catch it at construction and disable only the cava
module. The read_thread_ also calls loadConfig() at runtime, so contain the
throw there too, logging instead of terminating.
Fixes#4456
config_["hide-vacant"].asBool() was called from the river status listeners.
When the option is given as a string ("true") jsoncpp's asBool() throws
"Value is not convertible to bool", and that exception unwinding through
libwayland's C dispatch aborts the process. Parse it once in the constructor
into a bool member (accepting the string form) and read the cached value.
Fixes#4078
The state tier started actually running mpd in 74f0d33, which exposed a
deadlock in teardown: state.sh launches `mpd --no-daemon &` and never kills
it during the tier, and its cleanup() called smoke::stop *before* killing mpd.
smoke::stop ended in a bare `wait`, which reaps *every* background job of the
shell -- including the still-running mpd -- so it blocked until GitHub's 6h
job timeout. continue-on-error doesn't help: it catches failures, not hangs.
- lib.sh: smoke::stop now waits only on the PIDs it owns (waybar, compositor),
so an unrelated daemon left running by a tier can't deadlock teardown.
- state.sh: cleanup() tears down mpd/pulseaudio before smoke::stop.
- smoke.yml: add timeout-minutes: 20 so a future hang fails fast instead of
burning the default 6h runner budget.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first run went green but the best-effort tiers silently didn't exercise
anything:
- state.sh: the mpd `audio_output { ... }` block was on one line, which mpd
rejects ("Unknown tokens after '{'"), so mpd never started and the mpd
sub-tier (the #5183 empty-queue repro) skipped. Split the block across lines.
Also move teardown into a cleanup() with `|| true` so a dead-pid kill under
`set -e` no longer makes the step exit 1.
- hardware.sh: umockdev-run's LD_PRELOAD lands ahead of the linked-in ASan
runtime, so ASan aborted before main() ("ASan runtime does not come first")
and waybar never started -- the backlight/slider (#5179) path wasn't tested.
Set verify_asan_link_order=0 so the instrumented binary runs under the preload.
- leakcheck.sh: abort_on_error=1 turned LSan's exit report into a core dump;
force abort_on_error=0 for this report-only tier.
- lib.sh: assert_clean now also flags "ASan runtime does not come first" so a
future preload/link-order regression fails loudly instead of passing.
The smoke test only exercised steady-state rendering and killed waybar with
SIGTERM, so whole crash classes were invisible: exit-time use-after-free
(#5182), module state transitions (#5183) and hardware-backed modules (#5179).
Add tiers that hit those paths under ASan (+ _GLIBCXX_ASSERTIONS):
- lib.sh: assert_clean_exit (SIGINT teardown -> checks segfault/abort + ASan
report emitted during destruction), output hotplug helpers, signal/reload,
WAYBAR_WRAP hook, opt-in leak detection.
- lifecycle.sh (gating): clean exit, runtime output hotplug (Bar/module
destroy), toggle/reload churn, fast-interval teardown race (#5182 class).
- fuzz.sh (gating): pathological custom-backend output (empty, nonzero exit,
invalid JSON, huge, non-UTF8, empty format).
- coverage.sh (gating): every Factory module must be classified for smoke
coverage; a new unlisted module fails the job (#5179 slipped through).
- modules.sh: also render the whole matrix inside a group; SIGINT teardown per
module instead of SIGTERM.
- state.sh (best-effort): real mpd driven through an empty queue (#5183) +
stop/clear; pulseaudio + slider on a null sink.
- hardware.sh (best-effort): backlight, backlight/slider (#5179) and battery
via umockdev.
- leakcheck.sh (report-only): clean-exit run under LSan.
Members are destroyed in reverse declaration order, so modules_all_ (and
the modules it owns) are gone before the GtkWindow. Tearing down a mapped
window emits `unmap`, whose handler runs toggleSuspend() over the already
freed modules. Disconnect the map/unmap handlers in ~Bar first.
Fixes#5182
mpd_run_current_song() returns NULL when there is no current song (e.g.
after `mpc clear`), leaving song_ null. setLabel() always evaluates the
fmt::format() tag arguments -- even for format-stopped -- so getTag() and
getFilename() would call mpd_song_get_tag()/mpd_song_get_uri() on a null
song and segfault. Guard both against a null song_.
Fixes#5183
The un-ignore branch in hide_if_ignored() computed is_was_ignored
after the enclosing condition already required ignored_ to be false,
so it never ran, and a task whose app_id/title changed away from an
ignore-list entry stayed hidden forever.
Reset ignored_ and show the button again, subject to the same
all-outputs/output-membership check used everywhere else.
hide_if_duplicate() unconditionally re-ran handle_output_enter() with
the bar's own wl_output for every non-squashed task, so any app_id or
title event made the task visible on all bars and "all-outputs": false
was effectively ignored. The un-squash path in handle_closed() showed
the replacement task unconditionally, with the same effect.
Track whether the toplevel is actually on the bar's output from the
protocol's output_enter/output_leave events, split the button
show/hide logic out of the protocol handlers, and gate every synthetic
re-show on all-outputs or the tracked output membership.
Fixes#5178
The network module only populates the interface address from netlink
events (RTM_NEWADDR) or an explicit address dump. The interval timer
re-queries WiFi and bandwidth but never re-fetches the address, so the
module relies entirely on receiving the RTM_NEWADDR event.
Netlink multicast delivery is reliable unless the socket receive buffer
overflows, in which case the kernel drops notifications and reports
ENOBUFS. During a burst of link/address/route changes -- e.g. a router
reboot or a PPPoE redial -- this can drop the RTM_NEWADDR carrying the
interface's new IP (after the old one was removed by RTM_DELADDR). With
no overrun handling and no periodic resync, the address field stays
blank until Waybar is restarted (which re-dumps addresses).
Handle the overrun: when nl_recvmsgs_default reports ENOBUFS/NLE_NOMEM,
request a fresh link/address (and route, when auto-detecting) dump to
resynchronise, instead of silently continuing with lost state. Also
enlarge the event socket receive buffer to make overruns less likely in
the first place. The fix stays within the event thread, so it adds no
new locking or cross-thread socket access.
Fixes#5122.
The backlight udev worker thread called enumerate_devices() on every
epoll_wait timeout, i.e. once per polling interval. enumerate_devices()
runs udev_enumerate_scan_devices(), which walks the entire
/sys/class/backlight and /sys/class/leds trees and opens/closes the
sysfs root and every device path. With no `interval` configured the
module polls on its default cadence, so this full re-scan ran
continuously even when brightness never changed, flooding the
filesystem (observed via fatrace as constant open/close of `/`).
Raising `interval` only lowered the cadence, which is why the reporter's
`interval: 10` workaround reduced the flood.
The full re-enumeration is redundant: the udev monitor already delivers
change/add/remove events for the backlight and leds subsystems. On the
timeout path, re-read only the sysfs attributes of the devices already
tracked (via udev_device_new_from_subsystem_sysname) instead of
re-scanning the whole tree. This keeps periodic refresh working for
firmware backlights such as acpi_video that may not emit udev change
events, while eliminating the tree-wide scan. Device discovery of
new/removed devices continues through the udev monitor.
Fixes#5020.
Installing the bzip2 package does not ship a pkg-config file, so freetype2.pc
(a transitive requirement of gtkmm-3.0) still failed with 'Package bzip2 ...
not found'. bzip2 lives in the FreeBSD base system; write a minimal bzip2.pc
before meson setup so pkg-config can resolve it.