perf(platform): launch URL and folder openers without waiting for them

On Linux and macOS openUrl/openFolder spawned xdg-open/open and waited
for it on the UI thread: 50-300 ms per click where the opener forks and
exits, and on generic xdg-open sessions (sway, i3, WSLg, ...), which run
the browser or file manager as xdg-open's foreground child, the window
froze until the user closed that app. The opener is now left running
and a detached thread reaps it; the click returns at once.

Its stdin, stdout and stderr go to /dev/null again. That redirect was
lost in the switch to posix_spawnp, so a text-mode browser fallback
could take over the wallet's terminal.

The result now means "launched" rather than "exited 0" (no caller reads
it), and a non-zero opener exit is reported on stderr instead of in the
debug log. Windows (ShellExecute, which never waited) is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 21:09:48 -05:00
co-authored by Claude Opus 5.5
parent 0e2f853025
commit 8f8dc4c922
2 changed files with 92 additions and 16 deletions
+33 -16
View File
@@ -12,6 +12,8 @@
#include <fstream>
#include <sstream>
#include <filesystem>
#include <system_error>
#include <thread>
// Embedded ui.toml (generated at build time)
#if __has_include("ui_toml_embedded.h")
@@ -79,29 +81,44 @@ bool launchOpener(const char* opener, const std::string& target)
nullptr
};
// The opener must not share the wallet's stdio (a text-mode browser fallback such as lynx/w3m would
// grab the terminal); the old `sh -c '... >/dev/null 2>&1 &'` redirected these.
posix_spawn_file_actions_t fa;
const bool haveFa = (posix_spawn_file_actions_init(&fa) == 0);
if (haveFa) {
posix_spawn_file_actions_addopen(&fa, STDIN_FILENO, "/dev/null", O_RDONLY, 0);
posix_spawn_file_actions_addopen(&fa, STDOUT_FILENO, "/dev/null", O_WRONLY, 0);
posix_spawn_file_actions_addopen(&fa, STDERR_FILENO, "/dev/null", O_WRONLY, 0);
}
pid_t pid = 0;
int rc = posix_spawnp(&pid, opener, nullptr, nullptr, argv, environ);
int rc = posix_spawnp(&pid, opener, haveFa ? &fa : nullptr, nullptr, argv, environ);
if (haveFa) posix_spawn_file_actions_destroy(&fa);
if (rc != 0) {
DEBUG_LOGF("Failed to launch %s: %s\n", opener, std::strerror(rc));
return false;
}
int status = 0;
pid_t waited = 0;
do {
waited = waitpid(pid, &status, 0);
} while (waited < 0 && errno == EINTR);
if (waited < 0) {
DEBUG_LOGF("Failed waiting for %s launcher: %s\n", opener, std::strerror(errno));
return false;
// Never wait on the caller (the UI thread): generic xdg-open sessions (sway/i3/WSLg/...) run the
// browser / file manager as xdg-open's foreground child, so the old waitpid() froze the window until
// the user closed that app. A detached waiter reaps it instead. `opener` is always a string literal.
// No Logger in the waiter: it can wake during static teardown (tests), where Logger is gone.
try {
std::thread([pid, opener] {
int status = 0;
pid_t waited = 0;
do {
waited = waitpid(pid, &status, 0);
} while (waited < 0 && errno == EINTR);
if (waited >= 0 && (!WIFEXITED(status) || WEXITSTATUS(status) != 0))
std::fprintf(stderr, "Launcher %s exited with status %d\n", opener,
WIFEXITED(status) ? WEXITSTATUS(status) : -1);
}).detach();
} catch (const std::system_error&) {
int status = 0;
(void)waitpid(pid, &status, WNOHANG); // out of threads: worst case one zombie until exit
}
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
DEBUG_LOGF("Launcher %s exited with status %d\n", opener,
WIFEXITED(status) ? WEXITSTATUS(status) : -1);
return false;
}
return true;
return true; // "launched"; the opener's exit status is no longer awaited (no caller reads it)
}
#endif