Skip to content

Add Windows shell integration and tray mount manager#47

Merged
reinauer merged 10 commits into
reinauer:mainfrom
tbdye:phase8/shell-tray-launcher
Jun 28, 2026
Merged

Add Windows shell integration and tray mount manager#47
reinauer merged 10 commits into
reinauer:mainfrom
tbdye:phase8/shell-tray-launcher

Conversation

@tbdye

@tbdye tbdye commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds native Windows Explorer integration to AmiFUSE. Users can right-click .hdf and .adf files to mount them directly from Explorer, with a system tray icon that manages active mounts. This transforms AmiFUSE from a CLI-only tool into an Explorer-integrated experience on Windows.

The implementation includes a background launcher, system tray mount manager, working Windows unmount (via CTRL_BREAK_EVENT), a migration from deprecated wmic to CIM/PowerShell for mount discovery, and full Explorer file/directory manipulation support in write mode.

What's New

For users:

  • Right-click context menu on .hdf and .adf files: "Mount with AmiFUSE" (read-only) and "Mount Read-Write with AmiFUSE"
  • amifuse register — adds file type associations under HKCU\Software\Classes (no admin required)
  • amifuse unregister — cleanly removes all registry entries including empty key stubs
  • System tray icon appears while mounts are active, with per-mount Inspect/Unmount actions and "Unmount All"
  • File type icons for .hdf (amber hard disk) and .adf (green floppy), pretty-file-icons style
  • --background mode now works on Windows — daemon mode guard removed, WinFSP uid=-1/gid=-1 enables proper SID mapping for write mounts
  • Working amifuse unmount on Windows — discovers mount PIDs and sends CTRL_BREAK_EVENT for clean termination
  • Explorer file creation works in write mode — active-handle getattr fallback resolves the WinFSP timing race where fgetattr fires before the handler commits, plus FileInfoTimeout=1000 as defense-in-depth
  • Explorer file deletion works in write mode — deferred-delete pattern handles WinFSP's unlink-before-release ordering
  • Explorer directory deletion works in write mode — ACTION_INHIBIT cycle clears PFS3/SFS internal handler state, with deferred rmdir and cascading nested deletion

For maintainability:

  • wmic calls replaced with Get-CimInstance fallback (wmic is deprecated on Windows 11)
  • Kill/process functions extracted from fuse_fs.py to platform.py for reuse
  • fusepy child process deduplication prevents duplicate entries in tray and status output
  • Doctor checks tray dependencies (pystray/Pillow) when shell registration is active

Known Limitations

  • Explorer doesn't auto-refresh when drives appear/disappear (WinFSP limitation, needs SHChangeNotify)

How It Works

Explorer right-click → VBS wrapper → pythonw.exe launcher.py
                                          ├── Spawns mount process (background, detached)
                                          ├── Starts tray.py (single-instance via named mutex)
                                          └── os._exit() (instant exit, no Explorer hang)

Tray mount manager:
  - Polls for active AmiFUSE mounts (reuses platform.py process scanning)
  - Deduplicates fusepy child processes (parent_pid tracking)
  - Builds per-mount menu entries with factory callbacks
  - Unmount sends CTRL_BREAK_EVENT, with taskkill /F fallback
  - threading.Event wakes poll loop instantly on unmount
  - Auto-exits 10s after last mount removed

Flat context menu verbs, not cascading menus. Win11's modern context menu partially renders cascading submenus registered under HKCU — the parent item appears but the flyout is empty. Flat verbs work reliably in both classic and modern menus.

VBS wrapper for launcher. Explorer waits for directly-invoked executables to exit before releasing the context menu. A VBS wrapper launches pythonw.exe detached and exits immediately, preventing Explorer from hanging. CREATE_BREAKAWAY_FROM_JOB prevents Job Object restrictions from blocking the subprocess.

CTRL_BREAK_EVENT is the only clean unmount on Windows. Programmatic testing of 5 methods (FSCTL_DISMOUNT_VOLUME, mountvol /D, WinFSP launchctl, CTRL_BREAK_EVENT, taskkill) showed only CTRL_BREAK_EVENT produces a clean exit. WinFSP volumes don't support standard volume IOCTLs, aren't standard mount points, and don't respond to WM_CLOSE.

Deferred-delete for WinFSP compatibility. WinFSP calls FUSE unlink() during Cleanup (before release()), while the Amiga handler still holds an internal file lock. AmiFUSE defers the deletion to release(), which frees the handler lock first, then retries the delete.

Active-handle getattr fallback for file creation. WinFSP calls fgetattr immediately after create(), but the stat cache has TTL 0 in write mode and the Amiga handler hasn't committed yet -- producing ENOENT. When a file has an active open handle, getattr now returns synthetic attributes immediately instead of querying the handler. FileInfoTimeout=1000 in WinFSP mount options provides defense-in-depth by caching file attributes for 1 second.

Inhibit cycle for directory deletion. PFS3 and SFS maintain internal cache/reserved-block state after directory modifications. Even with zero volume locks, ACTION_DELETE_OBJECT returns OBJECT_IN_USE (202). An ACTION_INHIBIT cycle (inhibit then uninhibit) forces the handler to release all internal state -- the AmigaDOS-sanctioned way to clear handler caches. Directory deletion is deferred via opendir/releasedir tracking, with atomic bridge-level delete holding the lock across locate-parent + delete to prevent concurrent readdir from creating stale locks. Pending-delete entries are filtered from readdir and getattr results.

No .iso/.img registration. Windows handles these natively.

Files

File Description
amifuse/windows_shell.py (new) Registry operations, icon generation, register/unregister commands
amifuse/launcher.py (new) Background mount launcher for Explorer context menu
amifuse/tray.py (new) System tray mount manager
amifuse/platform.py Kill functions, CIM fallback, mount dedup, _pid_exists fix, driver size check, FileInfoTimeout in Windows mount options
amifuse/fuse_fs.py Kill function extraction, daemon mode guard removal, deferred-delete, path tracking in fh_cache, active-handle getattr fallback, opendir/releasedir tracking, deferred rmdir, inhibit cycle, atomic dir delete, pending-delete filtering
amifuse/doctor.py Tray dependency check (pystray/Pillow)
pyproject.toml pystray/Pillow optional deps, amifuse-launcher/amifuse-tray gui-scripts
README.md New subcommands, Windows shell integration section

Test Plan

Automated (all passing, 454 unit + 11 integration):

  • 16 unit tests for windows_shell.py — register, unregister, icon generation, edge cases
  • 19+ unit tests for tray.py — menu building, mount discovery, unmount, single instance, auto-exit, quit signal
  • Unit tests for launcher.py — subprocess spawning, logging, tray startup
  • Unit tests for platform.py additions — kill_pids, _pid_exists, dedup, CIM fallback, FileInfoTimeout assertion
  • Unit tests for doctor.py — tray dependency check
  • 5 integration tests — registry round-trip (test_windows_shell_integration.py)
  • 3 integration tests — detached process lifecycle (test_detached_process.py)
  • 3 integration tests — launcher E2E with PFS3 fixture (test_launcher_e2e.py)
  • Updated test_fuse_fs.py, test_platform.py, test_status.py for function extraction

Manual validation (2026-06-25):

  • amifuse register — context menu entries appear for .hdf/.adf in Explorer
  • Right-click .hdf → "Mount with AmiFUSE" — drive letter appears, no console flash, no Explorer hang
  • Tray icon appears with mounted drive in menu
  • Tray → Unmount — drive removed, tray auto-exits after 10s
  • Tray → Exit — tray exits cleanly (fixed deadlock)
  • Second tray launch exits silently (single-instance mutex)
  • amifuse unregister — all registry entries removed cleanly (no empty stubs)
  • Write mount: file deletion from Explorer works (deferred-delete)
  • Write mount: directory deletion from Explorer works (including non-empty)
  • Write mount: "New File" from Explorer works (active-handle fallback + FileInfoTimeout)
  • amifuse unmount D: from CLI — clean termination via CTRL_BREAK_EVENT

@tbdye tbdye force-pushed the phase8/shell-tray-launcher branch from 3687b32 to bd75bff Compare June 25, 2026 18:12
tbdye added 6 commits June 25, 2026 20:23
Switch from ofs_adf_image (requires FastFileSystem) to pfs3_image
with explicit --driver flag. Add DEVNULL for stdout/stderr to prevent
crash from invalid handles in DETACHED_PROCESS mode.
- Deferred-delete pattern: WinFSP calls unlink() before release(), so the
  Amiga handler reports ERROR_OBJECT_IN_USE (202). Defer deletion to
  release() when a file handle is active; also fixes a lock leak on error.
- Tray exit deadlock: icon.stop() from a pystray callback deadlocks the
  Win32 message pump. Signal the poll thread to call it instead.
- Launcher path resolution: find amifuse-launcher.exe via shutil.which and
  sysconfig for user-scheme and system installs, not just venv.
- Doctor tray deps check: warn when pystray/Pillow are missing but shell
  registration is active.
File creation ("New Text Document") failed because the stat cache
TTL is 0 in write mode, making create()'s cache priming dead code.
WinFSP's immediate fgetattr after create() fell through to the Amiga
handler which hadn't committed yet, returning ENOENT.

Directory deletion failed because PFS3 and SFS maintain internal
cache state after directory modifications that causes
ACTION_DELETE_OBJECT to return OBJECT_IN_USE even with zero locks
in the volume chain. An ACTION_INHIBIT cycle clears this state.

Changes:
- Active-handle fallback in getattr for recently-created files
- FileInfoTimeout=1000 in Windows mount options
- opendir/releasedir to track directory handles
- Deferred unlink/rmdir for WinFSP delete-before-release pattern
- Inhibit cycle before directory deletion to clear handler state
- Atomic delete_dir bridge method to prevent lock races
- readdir/getattr filtering of pending-delete entries
- Fix command injection via cmd.exe metacharacters in launcher and tray
  inspect actions (launch python directly with CREATE_NEW_CONSOLE)
- Replace CTRL_BREAK_EVENT with taskkill for detached process kill
  (CTRL_BREAK only works within same console group)
- Add PID reuse guard: verify process is python/amifuse before kill
- Fix race in _resolve_pending_dir_deletes (check dict under _fh_lock)
- Wrap all _pending_deletes mutations in _fh_lock for consistency
- Add null-handle check on CreateMutexW in tray single-instance
- Move inhibit_cycle in rmdir to error-recovery path only
- Guard releasedir deferred-delete against crashed/read-only handler
- Fix VBS launcher backslash escaping for MSVCRT argument parsing
- Eliminate TOCTOU in VBS launcher file verification
- Fix test_tray mocks to target actual ctypes.get_last_error API
@tbdye tbdye force-pushed the phase8/shell-tray-launcher branch from de536b5 to 993abc5 Compare June 26, 2026 03:25
@tbdye tbdye force-pushed the phase8/shell-tray-launcher branch from 50d2496 to 9ab62dd Compare June 26, 2026 20:01
@tbdye tbdye marked this pull request as ready for review June 26, 2026 20:09
@reinauer reinauer merged commit 8253733 into reinauer:main Jun 28, 2026
18 checks passed
@tbdye tbdye deleted the phase8/shell-tray-launcher branch June 29, 2026 18:23
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.

2 participants