From 7ae43310f4a5b2cf6cb0824ac7ab7333eb7f1eac Mon Sep 17 00:00:00 2001 From: VAIO73 <50487331+Vaso73@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:36:51 +0200 Subject: [PATCH] fix: recognize LXC mount targets with trailing slashes --- AppImage/components/virtual-machines.tsx | 7 +++- AppImage/messages/en/common.json | 3 ++ AppImage/messages/sk/common.json | 3 ++ AppImage/scripts/lxc_mount_points.py | 52 ++++++++++++++++-------- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/AppImage/components/virtual-machines.tsx b/AppImage/components/virtual-machines.tsx index e115863b..237b8849 100644 --- a/AppImage/components/virtual-machines.tsx +++ b/AppImage/components/virtual-machines.tsx @@ -606,6 +606,9 @@ function MountPointCard({ mp }: { mp: LxcMountPoint }) { }) const flags = optsEntries.filter((o) => o.value === null).map((o) => o.key) const keyValues = optsEntries.filter((o) => o.value !== null) as Array<{ key: string; value: string }> + const runtimeError = mp.runtime_error === "configured but not mounted" + ? t("vmLxc.details.mountErrors.configuredButNotMounted") + : mp.runtime_error return (
@@ -774,13 +777,13 @@ function MountPointCard({ mp }: { mp: LxcMountPoint }) { )} {/* Error / divergence note. */} - {mp.runtime_error && ( + {runtimeError && (

- {mp.runtime_error} + {runtimeError}

)}
diff --git a/AppImage/messages/en/common.json b/AppImage/messages/en/common.json index e4174605..7de17496 100644 --- a/AppImage/messages/en/common.json +++ b/AppImage/messages/en/common.json @@ -1044,6 +1044,9 @@ "stopped": "stopped", "mounted": "mounted" }, + "mountErrors": { + "configuredButNotMounted": "Configured but not mounted" + }, "startOnBoot": "Start at boot", "tags": "Tags", "tagsPlaceholder": "Add tag…", diff --git a/AppImage/messages/sk/common.json b/AppImage/messages/sk/common.json index 261f7b5d..40442c41 100644 --- a/AppImage/messages/sk/common.json +++ b/AppImage/messages/sk/common.json @@ -1044,6 +1044,9 @@ "stopped": "vypnuté", "mounted": "pripojené" }, + "mountErrors": { + "configuredButNotMounted": "Nastavené, ale nepripojené" + }, "startOnBoot": "Spúšťať pri štarte", "tags": "Značky", "tagsPlaceholder": "Pridať značku…", diff --git a/AppImage/scripts/lxc_mount_points.py b/AppImage/scripts/lxc_mount_points.py index f583664a..e3ec6c71 100644 --- a/AppImage/scripts/lxc_mount_points.py +++ b/AppImage/scripts/lxc_mount_points.py @@ -123,6 +123,20 @@ def _read_lxc_config(vmid: str) -> list[dict[str, Any]]: return out +def _mount_target_key(target: str) -> str: + """Return a stable comparison key for a CT-side mount target. + + Proxmox config accepts a trailing slash in ``mp=/path/`` while + ``/proc//mounts`` reports the realised target as ``/path``. + They name the same mount point, so comparisons must not treat the + spelling difference as a runtime divergence. Keep the root path + intact: stripping its only slash would turn it into an empty key. + """ + if target == "/": + return target + return target.rstrip("/") + + # --------------------------------------------------------------------------- # Type classification + source resolution # --------------------------------------------------------------------------- @@ -605,14 +619,14 @@ def get_lxc_mount_points_static(vmid: str) -> dict[str, Any]: if running and host_pid: try: config_targets = { - entry.get("target", "") + _mount_target_key(entry.get("target", "")) for entry in config_entries if entry.get("target") } for rt in _read_ct_proc_mounts(host_pid): if not _REMOTE_FS_RE.match(rt.get("rt_fstype", "")): continue - if rt.get("rt_target") in config_targets: + if _mount_target_key(rt.get("rt_target", "")) in config_targets: continue ad_hoc_hint_count += 1 except Exception: @@ -658,7 +672,9 @@ def get_lxc_mount_points_runtime(vmid: str) -> dict[str, Any]: # mount point are I/O-bound. Serialised, a CT with 5+ binds # tripped Caddy's 3s reverse-proxy timeout. from concurrent.futures import ThreadPoolExecutor - rt_by_target: dict[str, dict[str, Any]] = {m["rt_target"]: m for m in rt_mounts} + rt_by_target: dict[str, dict[str, Any]] = { + _mount_target_key(m["rt_target"]): m for m in rt_mounts + } runtime_by_target: dict[str, dict[str, Any]] = {} matched_targets: set[str] = set() @@ -673,9 +689,10 @@ def get_lxc_mount_points_runtime(vmid: str) -> dict[str, Any]: host_pid=host_pid if running else "", target=tgt, ) - live_target = bool(running and tgt and tgt in rt_by_target) + target_key = _mount_target_key(tgt) + live_target = bool(running and tgt and target_key in rt_by_target) health = _stat_via_host(host_pid, tgt) if live_target else None - return entry, capacity, live_target, health + return entry, capacity, target_key, live_target, health if config_entries: max_workers = max(2, min(8, len(config_entries))) @@ -684,11 +701,11 @@ def get_lxc_mount_points_runtime(vmid: str) -> dict[str, Any]: else: gathered = [] - for entry, cap, live_target, health in gathered: + for entry, cap, target_key, live_target, health in gathered: target = entry.get("target", "") rt_item: dict[str, Any] = {**cap} if live_target: - rt = rt_by_target[target] + rt = rt_by_target[target_key] rt_item.update({ "runtime_mounted": True, "runtime_source": rt["rt_source"], @@ -698,7 +715,7 @@ def get_lxc_mount_points_runtime(vmid: str) -> dict[str, Any]: "runtime_reachable": health["reachable"], "runtime_error": health["error"], }) - matched_targets.add(target) + matched_targets.add(target_key) elif running: rt_item["runtime_mounted"] = False rt_item["runtime_error"] = "configured but not mounted" @@ -712,7 +729,7 @@ def get_lxc_mount_points_runtime(vmid: str) -> dict[str, Any]: if running: ad_hoc_candidates = [ rt for rt in rt_mounts - if rt["rt_target"] not in matched_targets + if _mount_target_key(rt["rt_target"]) not in matched_targets and _REMOTE_FS_RE.match(rt["rt_fstype"]) ] if ad_hoc_candidates: @@ -776,7 +793,9 @@ def get_lxc_mount_points(vmid: str) -> dict[str, Any]: # Index runtime mounts by their CT-side target path so we can # match a config entry to its current realised state in O(1). - rt_by_target: dict[str, dict[str, Any]] = {m["rt_target"]: m for m in rt_mounts} + rt_by_target: dict[str, dict[str, Any]] = { + _mount_target_key(m["rt_target"]): m for m in rt_mounts + } out: list[dict[str, Any]] = [] matched_targets: set[str] = set() @@ -801,15 +820,16 @@ def get_lxc_mount_points(vmid: str) -> dict[str, Any]: target=tgt, ) host_src = _host_source_state(src) - live_target = bool(running and tgt and tgt in rt_by_target) + target_key = _mount_target_key(tgt) + live_target = bool(running and tgt and target_key in rt_by_target) health = _stat_via_host(host_pid, tgt) if live_target else None - return entry, classification, capacity, host_src, live_target, health + return entry, classification, capacity, host_src, target_key, live_target, health max_workers = max(2, min(8, len(config_entries) or 1)) with ThreadPoolExecutor(max_workers=max_workers) as pool: gathered = list(pool.map(_gather_one, config_entries)) - for entry, cls, cap, host_src, live_target, health in gathered: + for entry, cls, cap, host_src, target_key, live_target, health in gathered: source = entry.get("source", "") target = entry.get("target", "") @@ -830,7 +850,7 @@ def get_lxc_mount_points(vmid: str) -> dict[str, Any]: # Runtime enrichment when CT is up. if live_target: - rt = rt_by_target[target] + rt = rt_by_target[target_key] item.update({ "runtime_mounted": True, "runtime_source": rt["rt_source"], @@ -840,7 +860,7 @@ def get_lxc_mount_points(vmid: str) -> dict[str, Any]: "runtime_reachable": health["reachable"], "runtime_error": health["error"], }) - matched_targets.add(target) + matched_targets.add(target_key) elif running: # CT is running but the configured mount isn't in # /proc//mounts — divergence. Could be a startup @@ -860,7 +880,7 @@ def get_lxc_mount_points(vmid: str) -> dict[str, Any]: if running: ad_hoc_candidates = [ rt for rt in rt_mounts - if rt["rt_target"] not in matched_targets + if _mount_target_key(rt["rt_target"]) not in matched_targets and _REMOTE_FS_RE.match(rt["rt_fstype"]) ] # Same parallelisation as the configured-mp loop: stat'ing