Improve Explorer experience with disk space, shell notifications, and double-click mount#50
Merged
Merged
Conversation
Validate that the data buffer length is sufficient for the requested transfer count before issuing a SCSI WRITE. Previously, a short buffer was silently accepted, potentially writing incomplete data. Returns CHECK CONDITION with ILLEGAL_REQUEST / INVALID FIELD IN CDB sense data when the buffer is shorter than xfer_blocks * block_size.
Protect _stat_cache, _dir_cache, and _neg_cache with a single RLock to prevent undefined behavior when concurrent FUSE threads read/expire cache entries simultaneously. RLock chosen over Lock for FUSE callback reentrancy in multi-threaded mode (use_threads=True for read-only). Lock is never held during bridge calls (bridge has its own RLock), eliminating deadlock risk.
Extract the near-identical blocked-state handling from both the run_state.error and run_state.done branches of run_burst into a single _finalize_burst_blocked method. Pure refactor with one intentional normalization: exit_count is now reset uniformly in both branches when a blocked handler resumes (previously only reset in the done branch, which was an oversight).
Add HandlerBridge.get_disk_info() which sends ACTION_DISK_INFO to the Amiga handler and reads id_NumBlocks/id_NumBlocksUsed/id_BytesPerBlock from the InfoDataStruct response. AmigaFuseFS.statfs() maps these to POSIX statvfs fields so Explorer and df show correct disk geometry. Caching strategy: infinite for read-only mounts (geometry never changes), 30s TTL for writable.
Task 7a: Add admin-privilege detection (exits with re-launch guidance if not elevated), execution policy bypass for current process, and enhanced venv recovery that detects missing python.exe. Task 7b: Add -Uninstall switch that cleanly removes venv, registry keys (ProgIDs, file associations), and scheduled tasks. Runs amifuse unregister before venv removal. Idempotent and prints removal summary.
Add platform.notify_shell_drive_change() that fires SHCNE_DRIVEADD or SHCNE_DRIVEREMOVED so Explorer's sidebar updates immediately without manual refresh. Called from AmigaFuseFS.init() on mount, destroy() on unmount, and tray unmount handlers. Tray path is intentionally redundant for crash recovery (destroy never fires if the process dies).
Set shell\(Default) to "open" on AmiFUSE ProgIDs so double-clicking .hdf/.adf triggers mount. The launcher finds an available drive letter, spawns the mount daemon, polls for readiness (15s timeout), then opens Explorer to the mounted root. Shows MessageBox on failure.
New test_statfs.py verifies mounted images report non-zero geometry via os.statvfs (Unix) or GetDiskFreeSpaceExW (Windows). New test_write_persistence.py exercises writable mounts: immediate read-back, directory listing visibility, and cross-remount persistence. Extended test_mount_lifecycle.py with unmount-path-not-accessible test validating drive letter release on Windows / ismount on Unix.
Add Operating System :: Microsoft :: Windows and Python 3.9-3.13 version classifiers. Update README mount lifecycle bullet for double-click mount and add Windows Shell Integration section documenting context menu, tray, and uninstall workflows.
Add missing _check_handler_alive() to statfs, wrap SHChangeNotify and MessageBoxW args with c_wchar_p for explicit LPCWSTR marshaling, guard against negative free-block counts from corrupted FS metadata, and document lock ordering invariants.
- Change open verb (double-click) from read-only to read+write mount - Replace three context menu entries with two: "Mount & Open" (r/w default) and "Mount Read-Only" (secondary) - Clean up stale verb registry keys on re-register - Add install.bat wrapper with UAC auto-elevation for install-windows.ps1
…rrors Replace os.path.exists drive-letter probing with the Win32 GetLogicalDrives bitmask (new _windows_allocated_drive_letters helper, shared by auto-selection and validate_mountpoint). os.path.exists false-negatives on an assigned-but- empty removable slot (e.g. an empty card-reader D:), causing AmiFUSE to pick a letter WinFsp then refused with "mount point in use". Also surface launcher mount failures instead of exiting silently: _do_mount now selects its letter, polls for the mount, and starts the tray, and both _do_mount and _do_open show an error dialog on spawn failure or timeout with softened "did not appear yet -- may still be starting" wording.
If amifuse.fuse_fs is imported before the fuse_mock fixture runs, it binds
FUSE=None at import time and leaves fuse_fs unusable for later tests. Have
fuse_mock rebind fuse_fs's module-level FUSE symbols when the module is
already loaded (auto-reverted at teardown), and mark the test_status classes
that import fuse_fs with usefixtures("fuse_mock") so they can't pin FUSE=None
for tests that run after them.
Run install-windows.ps1 unelevated as the standard user so all user-scoped state (per-user Python, the venv, pip, HKCU shell registration) lands in the double-clicking user's profile and hive; elevate only the machine-wide WinFSP install (download MSI + SHA256-verify + elevate msiexec, pinned v2.1) rather than winget. Provision a wheel-supported per-user Python (3.9-3.13) instead of trusting system python, bypass setuptools_scm on the mapped-drive editable install via SETUPTOOLS_SCM_PRETEND_VERSION_FOR_AMIFUSE, and run a final read-only amifuse doctor health check. install.bat launches the .ps1 unelevated and holds the window open with the exit code.
On Windows, mount discovery tokenizes the command line with shlex.split(cmdline, posix=False), which keeps the surrounding quotes that subprocess.list2cmdline adds around paths containing spaces. Those literal quotes leaked into the reported image and mountpoint values. Strip only a single matched leading+trailing quote pair; leave unbalanced input (a quote on one side only) untouched. The Unix path (posix=True) carries no surrounding quotes, so this is a no-op there.
Introduce list_partitions plus the PartInfo and PartitionList dataclasses to surface every partition in an RDB image, keyed by drive name. Multi-RDB images (more than one 0x76 MBR partition) are walked across all RDBs, mirroring the existing multi-RDB loop; names, not indices, are the fan-out key because Partition.num restarts at 0 in each RDB and so collides across them. When a partition name collides across RDBs, name-keyed --partition routing would be ambiguous, so fall back to the first partition only and report the reason via PartitionList.fallback_reason. Both the rdisk and the underlying block device are closed after enumeration.
Add _select_drive_letters(n), returning up to n free letters plus the total available so the caller can mount best-effort on exhaustion; _select_drive_letter becomes a thin N=1 wrapper over it, sharing one drive-letter alphabet. Rewire _do_open and _do_mount onto a shared mount_image_all fan-out that enumerates partitions, allocates a letter each, spawns all mounts first, then polls a single aggregate deadline for the drives to appear. open mounts read-write and opens one Explorer window per confirmed drive; mountro mounts read-only and opens none. Failures are reported in at most one summary dialog, separating never-started spawns (definite) from drives that timed out (soft, may be a slow cold mount). N=1 reproduces the previous single-partition path byte-for-byte.
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.
Summary
Makes AmiFUSE behave as a first-class Windows drive in File Explorer:
statfsimplementation so Explorer Properties anddfshow correct volume size and free space, backed byACTION_DISK_INFOfrom the Amiga handlerSHChangeNotifynotifications so the sidebar updates immediately without manual refresh.hdf/.adfin Explorer mounts the image (read-write by default) and opens an Explorer window to the drive root--uninstallswitch for clean removal, and ainstall.batwrapper with UAC auto-elevationAlso fixes three low-severity bugs: SCSI WRITE(10) short-buffer validation, cache locking for multi-threaded read-only mounts, and duplicated resume logic in the startup runner.
What changed
fuse_fs.py,startup_runner.pystatfs()viaACTION_DISK_INFO; infinite TTL for read-only, 30s for writableplatform.py,fuse_fs.py,tray.pySHChangeNotifyon mount/unmount with crash-recovery redundancywindows_shell.py,launcher.pyinstall-windows.ps1,install.bat--uninstall, UAC wrapperscsi_device.pyfuse_fs.pyRLockprotecting stat/dir/neg caches in multi-threaded modestartup_runner.py_finalize_burst_blockedtest_statfs.py,test_write_persistence.py, + 6 morepyproject.toml,README.mdTest plan
pytest— 555 passed, 1 skipped (all green locally on Windows).adf— mounts r/w + Explorer opens to drive rootinstall.batdouble-click — UAC prompt, full install,doctorreports readyinstall.bat -Uninstall— clean removal, idempotent on re-run