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