|
16 | 16 | import sys |
17 | 17 | import time |
18 | 18 |
|
19 | | -# signal.SIGKILL is not defined on Windows; fall back to SIGTERM so that |
20 | | -# _kill_mount_owner_processes() can still compile and will use the strongest |
21 | | -# signal available. |
22 | | -_SIGKILL = getattr(signal, "SIGKILL", signal.SIGTERM) |
23 | 19 | from pathlib import Path |
24 | 20 | from typing import Dict, List, Optional, Tuple |
25 | 21 |
|
| 22 | +from .platform import _kill_mount_owner_processes |
| 23 | + |
26 | 24 | try: |
27 | 25 | from fuse import FUSE, FuseOSError, LoggingMixIn, Operations # type: ignore |
28 | 26 | except ImportError: |
@@ -1583,8 +1581,9 @@ def _track_op(self, op: str, path: str, cached: bool = False): |
1583 | 1581 |
|
1584 | 1582 | def _root_stat(self): |
1585 | 1583 | now = int(time.time()) |
| 1584 | + perm = 0o777 if self.bridge._write_enabled else 0o755 |
1586 | 1585 | return { |
1587 | | - "st_mode": (0o755 | 0o040000), # drwxr-xr-x |
| 1586 | + "st_mode": (perm | 0o040000), |
1588 | 1587 | "st_nlink": 2, |
1589 | 1588 | "st_size": 0, |
1590 | 1589 | "st_ctime": now, |
@@ -2501,11 +2500,6 @@ def mount_fuse( |
2501 | 2500 |
|
2502 | 2501 | if foreground is None: |
2503 | 2502 | foreground = plat.mount_runs_in_foreground_by_default() |
2504 | | - if not foreground and not plat.get_unmount_command(mountpoint): |
2505 | | - raise SystemExit( |
2506 | | - "Daemon mode is not supported on this platform yet because there is " |
2507 | | - "no standalone unmount command. Use --interactive instead." |
2508 | | - ) |
2509 | 2503 |
|
2510 | 2504 | # Print startup banner |
2511 | 2505 | print(__banner__) |
@@ -2568,6 +2562,12 @@ def mount_fuse( |
2568 | 2562 | "fsname": f"amifuse:{volname}", |
2569 | 2563 | "default_permissions": True, # Let kernel handle permission checks |
2570 | 2564 | } |
| 2565 | + # WinFSP maps FUSE uid=0/gid=0 to Windows SID S-1-5-0 (Nobody), blocking |
| 2566 | + # write access for the current user. uid/gid=-1 tells WinFSP to use the |
| 2567 | + # mounting user's SID instead. |
| 2568 | + if sys.platform.startswith("win"): |
| 2569 | + fuse_kwargs["uid"] = -1 |
| 2570 | + fuse_kwargs["gid"] = -1 |
2571 | 2571 | # subtype is a Linux-only FUSE option; WinFSP and macFUSE don't support it |
2572 | 2572 | if sys.platform.startswith("linux"): |
2573 | 2573 | fuse_kwargs["subtype"] = "amifuse" |
@@ -3802,12 +3802,13 @@ def cmd_unmount(args): |
3802 | 3802 | raise SystemExit(f"Mountpoint {mountpoint} is not currently mounted.") |
3803 | 3803 |
|
3804 | 3804 | cmd = plat.get_unmount_command(mountpoint) |
| 3805 | + _no_window = {"creationflags": 0x08000000} if sys.platform.startswith("win") else {} |
3805 | 3806 | if cmd: |
3806 | | - result = subprocess.run(cmd, check=False) |
| 3807 | + result = subprocess.run(cmd, check=False, **_no_window) |
3807 | 3808 | if result.returncode != 0: |
3808 | 3809 | killed_pids = _kill_mount_owner_processes(mountpoint) |
3809 | 3810 | if killed_pids: |
3810 | | - result = subprocess.run(cmd, check=False) |
| 3811 | + result = subprocess.run(cmd, check=False, **_no_window) |
3811 | 3812 | if result.returncode != 0: |
3812 | 3813 | raise SystemExit( |
3813 | 3814 | f"Unmount failed with exit code {result.returncode}: " |
@@ -3891,84 +3892,16 @@ def cmd_doctor(args): |
3891 | 3892 | _cmd_doctor(args) |
3892 | 3893 |
|
3893 | 3894 |
|
3894 | | -def _kill_mount_owner_processes(mountpoint: Path) -> List[int]: |
3895 | | - pids = _find_mount_owner_pids(mountpoint) |
3896 | | - if not pids: |
3897 | | - return [] |
3898 | | - |
3899 | | - remaining = [] |
3900 | | - for pid in pids: |
3901 | | - try: |
3902 | | - os.kill(pid, signal.SIGTERM) |
3903 | | - remaining.append(pid) |
3904 | | - except (ProcessLookupError, OSError): |
3905 | | - continue |
3906 | | - |
3907 | | - deadline = time.time() + 1.0 |
3908 | | - while remaining and time.time() < deadline: |
3909 | | - still_alive = [] |
3910 | | - for pid in remaining: |
3911 | | - if _pid_exists(pid): |
3912 | | - still_alive.append(pid) |
3913 | | - if not still_alive: |
3914 | | - return pids |
3915 | | - remaining = still_alive |
3916 | | - time.sleep(0.05) |
3917 | | - |
3918 | | - for pid in remaining: |
3919 | | - try: |
3920 | | - os.kill(pid, _SIGKILL) |
3921 | | - except (ProcessLookupError, OSError): |
3922 | | - continue |
3923 | | - |
3924 | | - return pids |
3925 | | - |
3926 | | - |
3927 | | -def _find_mount_owner_pids(mountpoint: Path) -> List[int]: |
3928 | | - """Find PIDs of amifuse processes that own the given mountpoint. |
3929 | | -
|
3930 | | - Uses platform.find_amifuse_mounts() for process discovery, then |
3931 | | - filters to those matching the given mountpoint. |
3932 | | - """ |
3933 | | - from . import platform as plat |
3934 | | - |
3935 | | - raw_mountpoint = str(mountpoint) |
3936 | | - abs_mountpoint = str(mountpoint.resolve(strict=False)) |
3937 | | - |
3938 | | - try: |
3939 | | - all_mounts = plat.find_amifuse_mounts() |
3940 | | - except OSError: |
3941 | | - return [] |
| 3895 | +def cmd_register(args): |
| 3896 | + """Handle the 'register' subcommand.""" |
| 3897 | + from .windows_shell import register |
| 3898 | + register() |
3942 | 3899 |
|
3943 | | - pids = [] |
3944 | | - for mount in all_mounts: |
3945 | | - mp = mount.get("mountpoint") |
3946 | | - if mp is None: |
3947 | | - continue |
3948 | | - if mp == raw_mountpoint or mp == abs_mountpoint: |
3949 | | - pids.append(mount["pid"]) |
3950 | | - continue |
3951 | | - try: |
3952 | | - resolved = str(Path(mp).expanduser().resolve(strict=False)) |
3953 | | - except OSError: |
3954 | | - continue |
3955 | | - if resolved == abs_mountpoint: |
3956 | | - pids.append(mount["pid"]) |
3957 | 3900 |
|
3958 | | - return pids |
3959 | | - |
3960 | | - |
3961 | | -def _pid_exists(pid: int) -> bool: |
3962 | | - try: |
3963 | | - os.kill(pid, 0) |
3964 | | - except ProcessLookupError: |
3965 | | - return False |
3966 | | - except PermissionError: |
3967 | | - return True |
3968 | | - except OSError: |
3969 | | - # Windows raises a generic OSError for invalid/dead PIDs |
3970 | | - return False |
3971 | | - return True |
| 3901 | +def cmd_unregister(args): |
| 3902 | + """Handle the 'unregister' subcommand.""" |
| 3903 | + from .windows_shell import unregister |
| 3904 | + unregister() |
3972 | 3905 |
|
3973 | 3906 |
|
3974 | 3907 | def _validate_driver_path(driver: Optional[Path]) -> None: |
@@ -4017,6 +3950,9 @@ def main(argv=None): |
4017 | 3950 | --json Output results as JSON. |
4018 | 3951 | --fix Auto-fix what can be fixed. |
4019 | 3952 |
|
| 3953 | + register Add Windows Explorer context menu entries. |
| 3954 | + unregister Remove Windows Explorer context menu entries. |
| 3955 | +
|
4020 | 3956 | format <image> <partition> [volname] |
4021 | 3957 | Format an Amiga partition. |
4022 | 3958 | --driver PATH Filesystem binary (default: extract from RDB). |
@@ -4176,6 +4112,17 @@ def main(argv=None): |
4176 | 4112 | ) |
4177 | 4113 | doctor_parser.set_defaults(func=cmd_doctor) |
4178 | 4114 |
|
| 4115 | + # register / unregister subcommands (Windows shell integration) |
| 4116 | + register_parser = subparsers.add_parser( |
| 4117 | + "register", help="Register AmiFUSE file associations (Windows)." |
| 4118 | + ) |
| 4119 | + register_parser.set_defaults(func=cmd_register) |
| 4120 | + |
| 4121 | + unregister_parser = subparsers.add_parser( |
| 4122 | + "unregister", help="Remove AmiFUSE file associations (Windows)." |
| 4123 | + ) |
| 4124 | + unregister_parser.set_defaults(func=cmd_unregister) |
| 4125 | + |
4179 | 4126 | # format subcommand |
4180 | 4127 | format_parser = subparsers.add_parser( |
4181 | 4128 | "format", help="Format an Amiga partition." |
|
0 commit comments