fix: various issues & improve performance - #269
Merged
0xbrayo merged 19 commits intoSep 23, 2026
Merged
Conversation
The parent-death pipe never worked: both ends lived in aw-tauri, so the monitor thread died with the parent and could not signal anything. What actually closed the write end was the module thread returning after its child had already exited, at which point the monitor sent SIGTERM to the old PID, which the OS may have already reused for an unrelated process. The pipe was also created without O_CLOEXEC, so every later module inherited earlier modules' pipe ends. Remove it. On Linux, use PR_SET_PDEATHSIG in pre_exec, which does what the pipe was meant to do. Windows keeps its job object; macOS has no equivalent, which is no regression since the pipe never worked there.
aw-notify is spawned with both stdout and stderr piped, but stderr was only read by wait_with_output() after stdout closed. aw-notify logs to stderr, so once ~64 KiB of logs built up the child blocked on its next write and stopped sending notifications. Read stderr on its own thread while stdout is consumed, keeping only the last 64 KiB for the crash log and the --output-only fallback check.
The spawn error branch checked the error for "No such option: --output-only", but spawn() errors come from the OS and never contain the child's usage output. The real fallback is the exit-code-2 stderr check after the child exits, so remove the dead branch.
restart_count only ever increased, so three crashes spread over weeks permanently disabled automatic restarts for a module. A module restarted by hand after hitting the limit also never auto-restarted again, and because "recovered" is keyed off restart_count > 0, every later start showed the crash-recovery alert. Reset the count when a module stops after running for at least 60s, and when the user starts it from the tray.
The crash-restart thread slept 2-8s and then restarted the module without re-checking state. If the user started and then stopped the module during that window, the retry started it again anyway; if it was started and crashed again, two retries could race to start it. Track a per-module start generation, bumped whenever a start is requested, and skip the retry if the module was started since the crash or a shutdown is pending.
A second instance writes the lock file with create+truncate, which usually fires both a Create and a Modify event. The first event removed the file, so remove_file().expect() panicked on the second, killing the watcher thread; later launches then no longer focused the window. Ignore NotFound as an already-handled event, and log rather than panic if showing the window fails.
The GUI setup duplicated prepare_aw_server() but opened the SQLite datastore (taking its lock) before checking that the port was free, the ordering prepare_aw_server() exists to avoid. It then panicked right after a non-blocking error dialog, so the dialog was never seen. Use prepare_aw_server() like mini mode does, and on failure show the error and exit once the user dismisses it.
Generic modules only had stdout piped, so stderr was inherited and the "Module X stderr:" line logged after a crash was always empty. The real error went to aw-tauri's own stderr, usually /dev/null for a GUI app. Pipe stderr and drain it on a thread, keeping the last 64 KiB.
wait_with_output() kept everything a module printed to stdout in memory until it exited, which for a watcher can be weeks, so any module that writes to stdout grew aw-tauri's memory without limit. Drain stdout on a thread like stderr and keep only the last 64 KiB, which is all the crash log uses.
build_menu_item() asked the OS for the login-item state each time the tray menu was rebuilt, which happens once at startup plus once per module that starts. With the macOS AppleScript launcher each query runs osascript against System Events, taking a hundred milliseconds or more. Cache the last state read from or written to the OS and use it for menu rebuilds. Explicit reads (the Tauri command and the toggle handler) still go to the OS and refresh the cache.
The 32 MiB limit was only checked at startup. aw-tauri usually runs for weeks, so the log could grow far past it in between, especially with a crash-looping module logging its stderr. Write through a small file wrapper that rotates once the file passes the limit. fern flushes after every record, so rotating on flush never splits a line across files.
Mini mode rebuilt the whole tray menu on every module start/stop and deep-copied the modules snapshot out of its Arc (the manager had also copied it just to wrap it in that Arc). Mirror the GUI path: keep the check items and only rebuild when a module joins the started group, otherwise just sync checkmarks. Pass the snapshot through by ownership instead of cloning it.
discover_modules() runs on the main thread during setup and called metadata() on every entry of every PATH directory before checking for the aw- prefix. On a typical macOS PATH that is ~3,400 stat calls to find ~20 candidates. Check the name first so only aw-* entries are stat'ed.
AppHandle is Clone and thread-safe, but it was stored behind a Mutex, and callers held that lock across slow main-thread work: the tray menu rebuild, notifications, showing windows. That serialized unrelated threads behind UI calls for no benefit. update_tray_menu() also held the HANDLE_CONDVAR lock for the whole menu build. Store the AppHandle directly, and release the condvar lock once the handle is known to be set. init_app_handle() now also notifies the condvar; before, only a Drop impl on a static (which never runs) did.
Discovery used the directory entry's own metadata, which doesn't follow symlinks. A symlink's mode is 0o777 on Linux (0o755 on macOS), so any aw-* link counted as an executable module, even one pointing at a non-executable or missing file, and symlinked aw-* directories were never searched. Stat the target instead, skipping broken links. The directory walk is split out of discover_modules() so it can be tested without PATH or the user config.
On macOS, std creates a child's stdio pipes with pipe() and only then sets close-on-exec (Linux uses pipe2(O_CLOEXEC)). Modules are spawned from their own threads, so at startup one module could inherit another module's pipe ends. When the owning module exited, the pipe stayed open, its reader never saw EOF, and Stopped wasn't sent until the other module exited too, leaving a stale PID in the manager and no crash restart. This predates the stderr capture, but piping stderr doubled the pipes per module. Take a lock around spawn() so pipe creation and exec can't interleave across module threads. The new stress test lost a Stopped message in 2 of 3 runs without the lock and passes consistently with it.
With no sender application set, mac-notification-sys resolves one on the first notification by compiling an AppleScript that looks up an app named "use_default". In mini mode that runs on the main thread from the tray event loop; it spun at ~95% CPU for over a minute and froze the tray, triggered by the first module-crash notification. Set the sender to our bundle identifier at startup so the lookup never runs. With the fix, mini mode stays idle through module crash notifications.
|
Startup sync cached the requested state as soon as enable()/disable() returned Ok, but those calls can silently do nothing (set_enabled reads back for that reason). The tray checkmark is built from the cache, so it could show "Start at login" enabled when it wasn't. Read the OS back after the startup change, and clear the cache whenever an apply fails so the next read goes to the OS.
Linux now uses PDEATHSIG and Windows a job object, but macOS had no way to clean up modules if aw-tauri crashed, was force-quit or SIGKILLed (the old pipe monitor never worked either), so they kept running unsupervised. Start a small /bin/sh watchdog as the leader of a new process group and have every module join it. The watchdog polls aw-tauri's pid once a second and, once it's gone, sends SIGTERM to the whole group, which also catches helpers the modules spawned themselves (aw-watcher-window's aw-watcher-window-macos). Verified by SIGKILLing a --testing daemon: all modules and the watchdog exited within a few seconds.
Member
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.