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.
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.
The FreeBSD build broke on 'Package bzip2, required by freetype2, not found':
freetype2.pc lists bzip2 as a (private) requirement, but no bzip2.pc was
installed, so pkg-config could not generate cflags for gtkmm-3.0. Add bzip2
to the package list.
The GtkBuilder created for menu construction was never unref'd on any
path (success or throw), leaking one builder per graph module with a
menu. Unref on each throw and at the end, and take an explicit ref on
menu_ so it survives dropping the builder (mirrors ALabel).
The per-menu-action string duplicated with g_strdup was never freed,
leaking one string per action on every menu build and reload. Use
g_signal_connect_data with (GClosureNotify)g_free so the copy is freed
when the closure is destroyed.
With a sink-mapping configured, sinkInfoCb could report the wrong sink's
volume depending on the order in which PulseAudio enumerated sinks during a
pa_context_get_sink_info_list sweep.
The mapping override was keyed on the mutable current_sink_name_ and ran
before the 'pick a running sink' fallback, which also mutated
current_sink_name_. If the default sink was running while the mapped target
was suspended, the fallback could reassign the selection to the default sink
after the mapping had already matched, so the reported sink depended on
enumeration order (and each sweep wrote the state twice, causing a flicker).
Resolve the target up front: key the mapping on the stable default_sink_name
and, when a mapping is in effect, treat the mapped target sink as the sole
definitive selection - every other sink is ignored and the running-sink
fallback is skipped. The default-sink + running-fallback behavior is
unchanged when no mapping applies. Verified in isolation across all sink
enumeration orders.
Every pa_operation* returned by the PulseAudio context introspection,
subscribe and volume/mute calls was discarded without pa_operation_unref,
leaking one operation object per call. Over a long session the periodic
subscription events accumulate an unbounded number of these handles.
Capture each returned handle and unref it (guarded against NULL) at every
discard site. Callback behavior is unchanged; these calls already run under
the threaded-mainloop lock, where unref is safe.
Turn the smoke test into a real runtime safety net. Waybar is now built with
AddressSanitizer and exercised end to end in a headless compositor:
- Tier 0: run everything under ASan; fail on ASan reports and Gtk/GLib
criticals in the log (lib.sh assert_clean)
- Tier 1: per-module render matrix (modules.sh) — each headless-safe module
rendered in isolation
- Tier 2: pointer interaction (interact.sh) — inject clicks via sway, assert
on-click side effect and format-alt toggle; AT-SPI assertions (a11y.sh/.py)
check module labels semantically instead of by pixels
- Tier 3: layout matrix (positions.sh) — top/bottom/left + HiDPI scale 2;
second compositor run under labwc
Shared helpers extracted to lib.sh. Adds a workflow_dispatch `bless` input to
regenerate the golden reference in one click. Trigger push only on master to
avoid duplicate PR runs. AT-SPI and labwc steps are continue-on-error.
MultipleImageStrategy::update() and handleClick() ran util::command::exec
(a blocking fork+exec+read) on the GTK main thread, so the whole bar froze
for the script's duration on every interval and on every click.
Move the exec into a new IStrategy::fetch() hook that the SleeperThread
worker runs before dp.emit(); update() now only parses the cached output
and draws on the main thread (mirroring how custom.cpp separates exec from
formatting). handleClick() uses forkExec() so clicks fire-and-forget instead
of blocking on the command's output. The entries and single-image paths are
unchanged.