diff --git a/CHANGELOG.md b/CHANGELOG.md
index df051b8..8739a3b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ Make the dashboard follow what each solver can actually do: panels and action bu
- `csauto doctor` reports the panels and capabilities derived for the configured solver
### Fixed
+- Opening the solver GUI on a case whose shared dirs are symlinks (the default since `mesh_mode = "symlink"` became the default in 0.4.1) left those symlinks dangling inside the container: `build_gui_command` (docker) and the singularity branch of `build_runtime_gui_command` mounted only the runs dir, unlike their `run` counterparts which also bind the symlink targets. Both now bind them the same way, `MESH` read-only and `POST` writable
- Requesting a restart on a solver without restart support returned HTTP 500 "Launch error", a client error reported as a server fault; it now returns HTTP 400 naming the solver
- Live control on a solver declaring no control action reported "Invalid action (expected one of [])"; both the API and the CLI now name the solver
- code_aster's Compare panel offered an empty file selector; it now offers `doe_row.csv`
diff --git a/csauto/docker.py b/csauto/docker.py
index fdaae5e..f9f6f5d 100644
--- a/csauto/docker.py
+++ b/csauto/docker.py
@@ -76,7 +76,11 @@ def build_gui_command(
except ValueError:
setup_rel = Path(setup_path.name)
container_setup = f"{container_case}/{setup_rel.as_posix()}"
+ from .execution import shared_dir_symlink_mounts
+
cmd: list[str] = ["docker", "run", "-v", f"{runs_root}:{container_root}"]
+ for target, readonly in shared_dir_symlink_mounts(runs_root, adapter.shared_dir_names):
+ cmd.extend(["-v", f"{target}:{target}:ro" if readonly else f"{target}:{target}"])
if display:
cmd.extend(
[
diff --git a/csauto/execution.py b/csauto/execution.py
index a8789d2..5fd97df 100644
--- a/csauto/execution.py
+++ b/csauto/execution.py
@@ -315,6 +315,8 @@ def build_runtime_gui_command(
"--pwd",
container_case,
]
+ for target, readonly in shared_dir_symlink_mounts(runs_root, adapter.shared_dir_names):
+ cmd.extend(["--bind", f"{target}:{target}:ro" if readonly else f"{target}:{target}"])
if os.environ.get("DISPLAY") and Path("/tmp/.X11-unix").exists():
cmd.extend(["--bind", "/tmp/.X11-unix:/tmp/.X11-unix"])
cmd.append(selection.singularity_image)
diff --git a/tests/unit/test_docker.py b/tests/unit/test_docker.py
index 6c25802..4dea8f2 100644
--- a/tests/unit/test_docker.py
+++ b/tests/unit/test_docker.py
@@ -2,7 +2,7 @@
from pathlib import Path
-from csauto.docker import build_run_command, read_container_id
+from csauto.docker import build_gui_command, build_run_command, read_container_id
def test_build_run_command_without_display(monkeypatch, tmp_path: Path) -> None:
@@ -71,3 +71,36 @@ def test_build_run_command_mounts_symlinked_shared_dirs(monkeypatch, tmp_path: P
assert f"-v {mesh.resolve()}:{mesh.resolve()}:ro" in joined
assert f"-v {post.resolve()}:{post.resolve()}" in joined
assert f"{post.resolve()}:ro" not in joined
+
+
+def test_build_gui_command_mounts_shared_dir_symlinks(monkeypatch, tmp_path: Path) -> None:
+ """The GUI must see the same shared dirs as a run, or symlinked meshes break inside the container."""
+ monkeypatch.delenv("DISPLAY", raising=False)
+ runs_dir = tmp_path / "RUNS"
+ case_dir = runs_dir / "case0001"
+ (case_dir / "DATA").mkdir(parents=True)
+ (case_dir / "DATA" / "setup.xml").write_text("", encoding="utf-8")
+ mesh = tmp_path / "study" / "MESH"
+ post = tmp_path / "study" / "POST"
+ mesh.mkdir(parents=True)
+ post.mkdir(parents=True)
+ (runs_dir / "MESH").symlink_to(mesh, target_is_directory=True)
+ (runs_dir / "POST").symlink_to(post, target_is_directory=True)
+
+ cmd = build_gui_command(case_dir, "my_image")
+
+ assert f"{mesh.resolve()}:{mesh.resolve()}:ro" in cmd
+ assert f"{post.resolve()}:{post.resolve()}" in cmd
+
+
+def test_build_gui_command_adds_no_mount_without_symlinks(monkeypatch, tmp_path: Path) -> None:
+ monkeypatch.delenv("DISPLAY", raising=False)
+ runs_dir = tmp_path / "RUNS"
+ case_dir = runs_dir / "case0001"
+ (case_dir / "DATA").mkdir(parents=True)
+ (case_dir / "DATA" / "setup.xml").write_text("", encoding="utf-8")
+ (runs_dir / "MESH").mkdir()
+
+ cmd = build_gui_command(case_dir, "my_image")
+
+ assert cmd.count("-v") == 1
diff --git a/tests/unit/test_execution.py b/tests/unit/test_execution.py
index 0d1b8bd..706914a 100644
--- a/tests/unit/test_execution.py
+++ b/tests/unit/test_execution.py
@@ -309,3 +309,29 @@ def test_build_singularity_slurm_script_binds_symlinked_shared_dirs(tmp_path: Pa
script = CodeSaturneAdapter().build_slurm_script(case_dir, nprocs=2, nt=1, selection=selection)
assert f"--bind {mesh.resolve()}:{mesh.resolve()}:ro" in script
+
+
+def test_build_runtime_gui_command_singularity_mounts_shared_dir_symlinks(tmp_path: Path, monkeypatch) -> None:
+ """The GUI must see the same shared dirs as a run, or symlinked meshes break inside the container."""
+ monkeypatch.delenv("DISPLAY", raising=False)
+ runs_dir = tmp_path / "RUNS"
+ case_dir = runs_dir / "case0001"
+ (case_dir / "DATA").mkdir(parents=True)
+ (case_dir / "DATA" / "setup.xml").write_text("", encoding="utf-8")
+ mesh = tmp_path / "study" / "MESH"
+ post = tmp_path / "study" / "POST"
+ mesh.mkdir(parents=True)
+ post.mkdir(parents=True)
+ (runs_dir / "MESH").symlink_to(mesh, target_is_directory=True)
+ (runs_dir / "POST").symlink_to(post, target_is_directory=True)
+ selection = RuntimeSelection(
+ runtime=RUNTIME_SINGULARITY,
+ docker_image="img",
+ singularity_bin="/usr/bin/apptainer",
+ singularity_image="/images/code_saturne.sif",
+ )
+
+ cmd = build_runtime_gui_command(case_dir, selection)
+
+ assert f"{mesh.resolve()}:{mesh.resolve()}:ro" in cmd
+ assert f"{post.resolve()}:{post.resolve()}" in cmd