Skip to content

Commit cce788f

Browse files
tbdyeclaude
andcommitted
feat: add amifuse status command for mount discovery
Enables programmatic mount lifecycle queries by discovering active AmiFUSE mounts through OS-level process inspection rather than PID files — reflecting actual system state without stale-file risk. Mount discovery uses `ps` on Unix and `wmic` on Windows to find amifuse processes, extracting mountpoint, image path, PID, and uptime from command-line tokens. The new `find_amifuse_mounts()` in platform.py serves as the single source of truth; the existing `_find_mount_owner_pids()` used by `cmd_unmount` is refactored as a thin filter wrapper to eliminate parser duplication. JSON output (`--json`) follows the established cmd_doctor schema pattern with nullable fields (`uptime_seconds`, `filesystem_type`) using explicit `null` rather than omission for consumer stability. Tests: 30 unit tests (mocked process output for both platforms, edge cases including malformed output, paths with spaces, OSError fallback) + 4 integration tests (CLI subprocess JSON schema validation). All 365 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7d9cce4 commit cce788f

5 files changed

Lines changed: 932 additions & 272 deletions

File tree

amifuse/fuse_fs.py

Lines changed: 70 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -3797,6 +3797,50 @@ def cmd_unmount(args):
37973797
)
37983798

37993799

3800+
def cmd_status(args):
3801+
"""Handle the 'status' subcommand."""
3802+
import json
3803+
from . import platform as plat
3804+
3805+
try:
3806+
mounts = plat.find_amifuse_mounts()
3807+
except OSError as exc:
3808+
if getattr(args, "json", False):
3809+
print(json.dumps({
3810+
"status": "error",
3811+
"command": "status",
3812+
"error": f"Process discovery failed: {exc}",
3813+
"mounts": [],
3814+
}, indent=2))
3815+
else:
3816+
print(f"Error: Process discovery failed: {exc}")
3817+
sys.exit(1)
3818+
3819+
if getattr(args, "json", False):
3820+
print(json.dumps({
3821+
"status": "ok",
3822+
"command": "status",
3823+
"mounts": mounts,
3824+
}, indent=2))
3825+
else:
3826+
if not mounts:
3827+
print("No active AmiFUSE mounts.")
3828+
else:
3829+
# Tabular output
3830+
print(f"{'PID':<8} {'Mountpoint':<20} {'Image':<40} {'Uptime'}")
3831+
print("-" * 80)
3832+
for m in mounts:
3833+
uptime = m.get("uptime_seconds")
3834+
if uptime is not None:
3835+
mins, secs = divmod(uptime, 60)
3836+
hrs, mins = divmod(mins, 60)
3837+
uptime_str = f"{hrs}h {mins}m {secs}s"
3838+
else:
3839+
uptime_str = "N/A"
3840+
image = m.get("image") or "N/A"
3841+
print(f"{m['pid']:<8} {m['mountpoint']:<20} {image:<40} {uptime_str}")
3842+
3843+
38003844
def cmd_doctor(args):
38013845
"""Handle the 'doctor' subcommand."""
38023846
import json
@@ -3946,136 +3990,35 @@ def _kill_mount_owner_processes(mountpoint: Path) -> List[int]:
39463990
def _find_mount_owner_pids(mountpoint: Path) -> List[int]:
39473991
"""Find PIDs of amifuse processes that own the given mountpoint.
39483992
3949-
Dispatches to platform-specific discovery: ``ps`` on Unix,
3950-
``wmic`` on Windows.
3993+
Uses platform.find_amifuse_mounts() for process discovery, then
3994+
filters to those matching the given mountpoint.
39513995
"""
3952-
if sys.platform.startswith("win"):
3953-
return _find_mount_owner_pids_windows(mountpoint)
3954-
return _find_mount_owner_pids_unix(mountpoint)
3955-
3956-
3957-
def _find_mount_owner_pids_windows(mountpoint: Path) -> List[int]:
3958-
"""Find amifuse PIDs on Windows using wmic."""
3959-
try:
3960-
result = subprocess.run(
3961-
["wmic", "process", "where",
3962-
"name like '%python%'",
3963-
"get", "ProcessId,CommandLine",
3964-
"/FORMAT:LIST"],
3965-
check=False,
3966-
capture_output=True,
3967-
text=True,
3968-
)
3969-
except OSError:
3970-
return []
3971-
if result.returncode != 0:
3972-
return []
3996+
from . import platform as plat
39733997

3974-
current_pid = os.getpid()
39753998
raw_mountpoint = str(mountpoint)
39763999
abs_mountpoint = str(mountpoint.resolve(strict=False))
3977-
pids = []
3978-
3979-
# wmic /FORMAT:LIST outputs key=value pairs separated by blank lines
3980-
current_cmdline = None
3981-
current_pid_val = None
3982-
for line in result.stdout.splitlines():
3983-
line = line.strip()
3984-
if not line:
3985-
# End of a record -- evaluate what we have
3986-
if current_cmdline is not None and current_pid_val is not None:
3987-
pid = current_pid_val
3988-
command = current_cmdline
3989-
if pid != current_pid and "amifuse" in command:
3990-
try:
3991-
tokens = shlex.split(command, posix=False)
3992-
except ValueError:
3993-
tokens = command.split()
3994-
if _command_matches_mountpoint(tokens, raw_mountpoint, abs_mountpoint):
3995-
pids.append(pid)
3996-
current_cmdline = None
3997-
current_pid_val = None
3998-
continue
3999-
if line.startswith("CommandLine="):
4000-
current_cmdline = line[len("CommandLine="):]
4001-
elif line.startswith("ProcessId="):
4002-
try:
4003-
current_pid_val = int(line[len("ProcessId="):])
4004-
except ValueError:
4005-
current_pid_val = None
4006-
4007-
# Handle last record if no trailing blank line
4008-
if current_cmdline is not None and current_pid_val is not None:
4009-
pid = current_pid_val
4010-
command = current_cmdline
4011-
if pid != current_pid and "amifuse" in command:
4012-
try:
4013-
tokens = shlex.split(command, posix=False)
4014-
except ValueError:
4015-
tokens = command.split()
4016-
if _command_matches_mountpoint(tokens, raw_mountpoint, abs_mountpoint):
4017-
pids.append(pid)
4018-
4019-
return pids
40204000

4021-
4022-
def _find_mount_owner_pids_unix(mountpoint: Path) -> List[int]:
4023-
"""Find amifuse PIDs on Unix using ps."""
40244001
try:
4025-
result = subprocess.run(
4026-
["ps", "-axo", "pid=,command="],
4027-
check=False,
4028-
capture_output=True,
4029-
text=True,
4030-
)
4002+
all_mounts = plat.find_amifuse_mounts()
40314003
except OSError:
40324004
return []
4033-
if result.returncode != 0:
4034-
return []
40354005

4036-
current_pid = os.getpid()
4037-
raw_mountpoint = str(mountpoint)
4038-
abs_mountpoint = str(mountpoint.resolve(strict=False))
40394006
pids = []
4040-
4041-
for line in result.stdout.splitlines():
4042-
line = line.strip()
4043-
if not line:
4007+
for mount in all_mounts:
4008+
mp = mount.get("mountpoint")
4009+
if mp is None:
40444010
continue
4045-
try:
4046-
pid_str, command = line.split(None, 1)
4047-
pid = int(pid_str)
4048-
except ValueError:
4049-
continue
4050-
if pid == current_pid or "amifuse" not in command:
4011+
if mp == raw_mountpoint or mp == abs_mountpoint:
4012+
pids.append(mount["pid"])
40514013
continue
40524014
try:
4053-
tokens = shlex.split(command)
4054-
except ValueError:
4055-
continue
4056-
if not _command_matches_mountpoint(tokens, raw_mountpoint, abs_mountpoint):
4057-
continue
4058-
pids.append(pid)
4059-
4060-
return pids
4061-
4062-
4063-
def _command_matches_mountpoint(tokens: List[str], raw_mountpoint: str, abs_mountpoint: str) -> bool:
4064-
for idx, token in enumerate(tokens):
4065-
if token != "--mountpoint":
4066-
continue
4067-
if idx + 1 >= len(tokens):
4068-
return False
4069-
mount_arg = tokens[idx + 1]
4070-
if mount_arg == raw_mountpoint or mount_arg == abs_mountpoint:
4071-
return True
4072-
try:
4073-
resolved = str(Path(mount_arg).expanduser().resolve(strict=False))
4015+
resolved = str(Path(mp).expanduser().resolve(strict=False))
40744016
except OSError:
40754017
continue
40764018
if resolved == abs_mountpoint:
4077-
return True
4078-
return False
4019+
pids.append(mount["pid"])
4020+
4021+
return pids
40794022

40804023

40814024
def _pid_exists(pid: int) -> bool:
@@ -4130,6 +4073,9 @@ def main(argv=None):
41304073
41314074
unmount <mountpoint> Unmount an existing AmiFUSE mount.
41324075
4076+
status Show active AmiFUSE mounts on this system.
4077+
--json Output results as JSON.
4078+
41334079
doctor Check prerequisites and environment readiness.
41344080
--json Output results as JSON.
41354081
@@ -4268,6 +4214,16 @@ def main(argv=None):
42684214
unmount_parser.add_argument("mountpoint", type=Path, help="Mounted filesystem path")
42694215
unmount_parser.set_defaults(func=cmd_unmount)
42704216

4217+
# status subcommand
4218+
status_parser = subparsers.add_parser(
4219+
"status", help="Show active AmiFUSE mounts on this system."
4220+
)
4221+
status_parser.add_argument(
4222+
"--json", action="store_true",
4223+
help="Output results as JSON.",
4224+
)
4225+
status_parser.set_defaults(func=cmd_status)
4226+
42714227
# doctor subcommand
42724228
doctor_parser = subparsers.add_parser(
42734229
"doctor", help="Check prerequisites and environment readiness."

0 commit comments

Comments
 (0)