Reconciler: uid/gid and ntfsfix for NTFS secondary mounts.

QA on 10.0.0.8 (mount-issues.md findings 2 and 3) showed the reconciler
path missed both NTFS accommodations that mount_drive() and
add_fstab_entry() already have: secondary NTFS drives mounted owned by
root (writable only via ntfs-3g's permissive default), and a dirty NTFS
volume would fail to mount at boot with no cleanup attempted.

Inject uid/gid for ntfs/ntfs3 in the reconciler, and run ntfsfix -d in
SubprocessMountExecutor before NTFS mounts (translating UUID=/LABEL=
specs to /dev/disk/by-* paths, which ntfsfix needs).  Also return an
error MountResult instead of raising when a binary is missing, per the
executor protocol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Roland 2026-07-06 16:52:11 -06:00
parent 9f2d9ed82b
commit 910503b641
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
4 changed files with 102 additions and 3 deletions

View file

@ -124,6 +124,67 @@ class TestSubprocessMountExecutor:
assert calls == [["mount", "-t", "ext4", "-o", "defaults",
"UUID=1", "/media/x"]]
def test_ntfs_mount_runs_ntfsfix_first(self, monkeypatch):
"""NTFS mounts run ntfsfix -d first to clear the Windows dirty flag;
UUID= specs are translated to /dev/disk/by-uuid for ntfsfix."""
calls = []
def fake_run(cmd, **kwargs):
calls.append(cmd)
return _FakeCompletedProcess(returncode=0)
monkeypatch.setattr(
"controller.lib.mount_executor.subprocess.run", fake_run,
)
executor = SubprocessMountExecutor()
result = executor.mount("UUID=ABC123", "/media/x", "ntfs", "defaults")
assert result == MountResult(ok=True)
assert calls[0] == ["ntfsfix", "-d", "/dev/disk/by-uuid/ABC123"]
assert calls[1][0] == "mount"
def test_ntfsfix_failure_does_not_block_mount(self, monkeypatch):
def fake_run(cmd, **kwargs):
if cmd[0] == "ntfsfix":
return _FakeCompletedProcess(returncode=1, stderr="dirty")
return _FakeCompletedProcess(returncode=0)
monkeypatch.setattr(
"controller.lib.mount_executor.subprocess.run", fake_run,
)
executor = SubprocessMountExecutor()
result = executor.mount("/dev/sda1", "/media/x", "ntfs3", "defaults")
assert result == MountResult(ok=True)
def test_ext4_mount_does_not_run_ntfsfix(self, monkeypatch):
calls = []
def fake_run(cmd, **kwargs):
calls.append(cmd)
return _FakeCompletedProcess(returncode=0)
monkeypatch.setattr(
"controller.lib.mount_executor.subprocess.run", fake_run,
)
executor = SubprocessMountExecutor()
executor.mount("UUID=1", "/media/x", "ext4", "defaults")
assert len(calls) == 1
assert calls[0][0] == "mount"
def test_missing_binary_returns_error_instead_of_raising(self, monkeypatch):
"""The executor protocol forbids raising for I/O failures; a missing
binary (FileNotFoundError) must come back as MountResult(ok=False)."""
def fake_run(cmd, **kwargs):
raise FileNotFoundError(2, "No such file or directory", cmd[0])
monkeypatch.setattr(
"controller.lib.mount_executor.subprocess.run", fake_run,
)
executor = SubprocessMountExecutor()
result = executor.mount("UUID=1", "/media/x", "ext4", "defaults")
assert result.ok is False
assert "No such file" in result.error
def test_mount_propagates_stderr(self, monkeypatch):
def fake_run(cmd, **kwargs):
return _FakeCompletedProcess(returncode=32, stderr="busy")