From b5f09b8b1eb85d73faa5122df58cb78f3eb0d8a9 Mon Sep 17 00:00:00 2001 From: probonopd Date: Fri, 2 Oct 2026 01:31:24 +0000 Subject: [PATCH 1/2] Keep FUSE mount alive while processes still use it The mount was torn down as soon as every holder of the keepalive pipe's read end was gone. Launchers that close inherited fds (e.g. sandboxing ones) while the app keeps running caused 'Transport endpoint is not connected' errors. Before terminating, wait until no process has its cwd, exe, open files or mappings inside the mount. Fixes #144 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01PvYeShdbCf9Row4sKDiMMj --- src/runtime/runtime.c | 112 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/src/runtime/runtime.c b/src/runtime/runtime.c index a073f60..4ed2593 100644 --- a/src/runtime/runtime.c +++ b/src/runtime/runtime.c @@ -56,6 +56,7 @@ extern int sqfs_opt_proc(void* data, const char* arg, int key, struct fuse_args* #include #include #include +#include #include #include #include @@ -588,6 +589,99 @@ extern int fusefs_main(int argc, char* argv[], void (* mounted)(void)); static pid_t fuse_pid; static int keepalive_pipe[2]; +static const char* fuse_mount_path = NULL; + +/* Returns true if path is the mount point or lies below it. */ +static bool +path_in_mount(const char* path, const char* mount) { + size_t len = strlen(mount); + return strncmp(path, mount, len) == 0 && (path[len] == '\0' || path[len] == '/'); +} + +/* Returns true if the process referenced by the /proc/ dir fd uses the mount through its cwd, root, exe, + * open files or memory mappings. */ +static bool +process_uses_mount(int proc_fd, const char* mount) { + char buf[PATH_MAX]; + const char* links[] = {"cwd", "root", "exe"}; + + for (size_t i = 0; i < sizeof(links) / sizeof(links[0]); i++) { + ssize_t n = readlinkat(proc_fd, links[i], buf, sizeof(buf) - 1); + if (n > 0) { + buf[n] = '\0'; + if (path_in_mount(buf, mount)) + return true; + } + } + + int fd_dir_fd = openat(proc_fd, "fd", O_RDONLY | O_DIRECTORY); + if (fd_dir_fd != -1) { + DIR* fds = fdopendir(fd_dir_fd); + if (fds != NULL) { + struct dirent* e; + while ((e = readdir(fds)) != NULL) { + if (e->d_name[0] == '.') + continue; + ssize_t n = readlinkat(fd_dir_fd, e->d_name, buf, sizeof(buf) - 1); + if (n > 0) { + buf[n] = '\0'; + if (path_in_mount(buf, mount)) { + closedir(fds); + return true; + } + } + } + closedir(fds); + } else { + close(fd_dir_fd); + } + } + + int maps_fd = openat(proc_fd, "maps", O_RDONLY); + if (maps_fd != -1) { + FILE* maps = fdopen(maps_fd, "r"); + if (maps != NULL) { + char line[PATH_MAX + 128]; + size_t mlen = strlen(mount); + while (fgets(line, sizeof(line), maps) != NULL) { + char* hit = strstr(line, mount); + if (hit != NULL && (hit[mlen] == '/' || hit[mlen] == '\n' || hit[mlen] == ' ')) { + fclose(maps); + return true; + } + } + fclose(maps); + } else { + close(maps_fd); + } + } + + return false; +} + +/* Returns true if any process other than ourselves still uses the mount. */ +static bool +mount_in_use(const char* mount) { + DIR* proc = opendir("/proc"); + if (proc == NULL) + return false; + + bool in_use = false; + struct dirent* e; + while (!in_use && (e = readdir(proc)) != NULL) { + if (e->d_name[0] < '0' || e->d_name[0] > '9') + continue; + if ((pid_t) atoi(e->d_name) == getpid()) + continue; + int proc_fd = openat(dirfd(proc), e->d_name, O_RDONLY | O_DIRECTORY); + if (proc_fd == -1) + continue; + in_use = process_uses_mount(proc_fd, mount); + close(proc_fd); + } + closedir(proc); + return in_use; +} static void* write_pipe_thread(void* arg) { @@ -599,6 +693,22 @@ write_pipe_thread(void* arg) { /* Write until we block, on broken pipe, exit */ res = write(keepalive_pipe[1], c, sizeof(c)); if (res == -1) { + /* All holders of the read end are gone. Some launchers close inherited file descriptors (e.g. when + * they sandbox the app) while the app is still running from the mount, so only shut down once nothing + * uses the mount any more. Otherwise the app sees "Transport endpoint is not connected". + * See https://github.com/AppImage/type2-runtime/issues/144 */ + char* real_mount = fuse_mount_path != NULL ? realpath(fuse_mount_path, NULL) : NULL; + if (real_mount != NULL) { + int idle_checks = 0; + while (idle_checks < 3) { + if (mount_in_use(real_mount)) + idle_checks = 0; + else + idle_checks++; + usleep(500 * 1000); + } + free(real_mount); + } kill(fuse_pid, SIGTERM); break; } @@ -1723,6 +1833,8 @@ int main(int argc, char* argv[]) { exit(EXIT_EXECERROR); } + fuse_mount_path = mount_dir; + if (pipe(keepalive_pipe) == -1) { perror("pipe error"); exit(EXIT_EXECERROR); From 9dc5b93fb578ee6296a1d9b956bf65e6070ffe38 Mon Sep 17 00:00:00 2001 From: probonopd Date: Thu, 15 Oct 2026 08:40:02 +0200 Subject: [PATCH 2/2] Make the wait for an unused mount documented and backed off The fixed 3 x 500 ms polling had no stated rationale and no backoff, so the daemon either scanned /proc too often while an app ran or reacted arbitrarily slowly. The wait now lives in its own function with named constants: it polls with exponential backoff (100 ms to 2 s) while the mount is busy and only tears it down after it has been unused for a 1 s quiet period, which bridges the gap between a launcher exiting and the next process starting. --- src/runtime/runtime.c | 63 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/src/runtime/runtime.c b/src/runtime/runtime.c index 4ed2593..5ec8f3e 100644 --- a/src/runtime/runtime.c +++ b/src/runtime/runtime.c @@ -57,6 +57,7 @@ extern int sqfs_opt_proc(void* data, const char* arg, int key, struct fuse_args* #include #include #include +#include #include #include #include @@ -683,6 +684,55 @@ mount_in_use(const char* mount) { return in_use; } +/* While the mount is in use, poll with exponential backoff between these bounds: a running app keeps the mount + * busy for a long time, so there is no point in scanning /proc often, but the cap keeps the reaction time to the + * app's exit short. */ +#define MOUNT_POLL_MIN_USEC (100 * 1000) +#define MOUNT_POLL_MAX_USEC (2 * 1000 * 1000) +/* The mount must stay unused for this long before it is torn down. This bridges the gap in which a launcher + * (e.g. a shell script or sandbox wrapper) has exited and the next process that uses the mount has not started + * yet, and avoids tearing the mount down in the middle of such a handover. */ +#define MOUNT_QUIET_USEC (1000 * 1000) + +static long +elapsed_usec(const struct timespec* since) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (now.tv_sec - since->tv_sec) * 1000000L + (now.tv_nsec - since->tv_nsec) / 1000L; +} + +/* Blocks until no other process uses the mount for MOUNT_QUIET_USEC. Returns immediately if the mount path cannot + * be resolved, as there is nothing to watch then. */ +static void +wait_until_mount_unused(void) { + char* real_mount = fuse_mount_path != NULL ? realpath(fuse_mount_path, NULL) : NULL; + if (real_mount == NULL) + return; + + useconds_t poll_usec = MOUNT_POLL_MIN_USEC; + struct timespec idle_since; + bool idle = false; + + while (1) { + if (mount_in_use(real_mount)) { + idle = false; + usleep(poll_usec); + poll_usec = poll_usec * 2 > MOUNT_POLL_MAX_USEC ? MOUNT_POLL_MAX_USEC : poll_usec * 2; + } else { + if (!idle) { + clock_gettime(CLOCK_MONOTONIC, &idle_since); + idle = true; + } + if (elapsed_usec(&idle_since) >= MOUNT_QUIET_USEC) + break; + /* Probe quickly while idle so the quiet period is checked with fine granularity. */ + poll_usec = MOUNT_POLL_MIN_USEC; + usleep(poll_usec); + } + } + free(real_mount); +} + static void* write_pipe_thread(void* arg) { char c[32]; @@ -697,18 +747,7 @@ write_pipe_thread(void* arg) { * they sandbox the app) while the app is still running from the mount, so only shut down once nothing * uses the mount any more. Otherwise the app sees "Transport endpoint is not connected". * See https://github.com/AppImage/type2-runtime/issues/144 */ - char* real_mount = fuse_mount_path != NULL ? realpath(fuse_mount_path, NULL) : NULL; - if (real_mount != NULL) { - int idle_checks = 0; - while (idle_checks < 3) { - if (mount_in_use(real_mount)) - idle_checks = 0; - else - idle_checks++; - usleep(500 * 1000); - } - free(real_mount); - } + wait_until_mount_unused(); kill(fuse_pid, SIGTERM); break; }