Skip to content

Keep FUSE mount alive while processes still use it - #154

Open
probonopd wants to merge 2 commits into
mainfrom
claude/friendly-shannon-a81gwh
Open

probonopd wants to merge 2 commits into
mainfrom
claude/friendly-shannon-a81gwh

Conversation

@probonopd

@probonopd probonopd commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Problem

Fixes #144. Apps such as the Bottles AppImage fail when run mounted but work with APPIMAGE_EXTRACT_AND_RUN=1, with errors like OSError: [Errno 107] Transport endpoint is not connected: '/usr/lib/python3.13/...' (see ivan-hc/Bottles-appimage#20).

Cause

The FUSE daemon writes to a keepalive pipe and terminates itself on EPIPE, i.e. as soon as every holder of the pipe's read end is gone. The read end is only held via fd inheritance, so any launcher that closes inherited fds (sandboxes/containers such as the JuNest/bwrap setup used by Bottles) drops it while the app is still running from the mount. The daemon then kills the mount underneath the app.

Fix

On EPIPE, the daemon no longer terminates immediately. It scans /proc for processes whose cwd, root, exe, open fds or memory maps lie inside the mount and only terminates after the mount has been unused for a 1s quiet period (polling with exponential backoff, 100ms to 2s, while it is in use).

Testing

I could not build the full runtime or reproduce with Bottles in my environment (squashfuse headers unavailable). I only compiled and ran the detection helpers and the wait loop standalone: they report a mount as in use when a process has its cwd there and as unused otherwise. This needs verification against the Bottles reproduction before merging.

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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PvYeShdbCf9Row4sKDiMMj
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Build for testing:
artifacts armhf
artifacts i686
artifacts x86_64
artifacts aarch64
Use at your own risk.

@probonopd
probonopd requested a review from TheAssassin October 14, 2026 06:48

@TheAssassin TheAssassin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, the code could be structured a bit better and there is a dire need for better in-line documentation. For instance, the sleeping is quite arbitrary and it's not clear why one would just wait for 3 attempts and why there shouldn't be variable backoff times (pretty standard nowadays)

Comment thread src/runtime/runtime.c Outdated
idle_checks = 0;
else
idle_checks++;
usleep(500 * 1000);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of a sync wait here, but it should be fine. Why have such a quick timeout, though? What's the rationale behind these figures?

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.
@probonopd

Copy link
Copy Markdown
Member Author

Thanks for the review. Pushed an update:

  • The wait is moved into its own function, wait_until_mount_unused(), with named constants and comments explaining each figure.
  • Backoff: while the mount is in use the poll interval doubles from 100 ms up to a 2 s cap. A running app keeps the mount busy for a long time, so scanning /proc often is pointless, and the cap keeps the reaction time after the app exits short.
  • The old "3 attempts" is replaced by a 1 s quiet period (time-based, not count-based). Its purpose is to bridge the gap where a launcher or wrapper has exited and the next process that uses the mount has not started yet; once nothing has used the mount for that long, it is torn down as before.
  • The wait is still synchronous, but it runs only in the keepalive thread after EPIPE, so it blocks nothing else.

The wait loop was only tested standalone (returns after ~1.1 s on an unused mount); a verification against the Bottles reproduction is still needed.

@github-actions

Copy link
Copy Markdown

Build for testing:
artifacts aarch64
artifacts i686
artifacts armhf
artifacts x86_64
Use at your own risk.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Some apps can break unless it is run extracted

2 participants