From 40ff688416a4887077b5418ec57557e8706466ad Mon Sep 17 00:00:00 2001 From: Emil Date: Mon, 3 Aug 2026 03:23:28 +0300 Subject: [PATCH] Probe the managed venv in the wizard preflight and document RDKit's X11 deps --- README.md | 4 +++ coordinator/cmd/worker-agent/main.go | 2 +- coordinator/internal/agent/check.go | 33 +++++++++++++------ coordinator/internal/agent/setupui/server.go | 5 ++- .../internal/agent/setupui/server_test.go | 25 ++++++++++++++ mkdocs/standalone.md | 7 ++++ 6 files changed, 64 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 038ee4e..55496bf 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,10 @@ The coordinator and worker agent are Go modules under `coordinator/` and `users/ cd coordinator && make coordinator agent && go test ./... ``` +On headless servers (no desktop environment), RDKit needs a few X11 +libraries that desktops already ship: `sudo apt-get install -y libxrender1 +libxext6 libxcursor1 libxfixes3 libxi6 libxrandr2`. + `make check` runs the full gate: vet, lint, race tests, the PostgreSQL integration suite, and the two-worker end-to-end smoke script. diff --git a/coordinator/cmd/worker-agent/main.go b/coordinator/cmd/worker-agent/main.go index b107428..f2c15b3 100644 --- a/coordinator/cmd/worker-agent/main.go +++ b/coordinator/cmd/worker-agent/main.go @@ -58,7 +58,7 @@ func main() { fmt.Println("check: no coordinator URL (pass --coordinator-url or set COORDINATOR_URL)") os.Exit(1) } - report := agent.RunCheck(ctx, url) + report := agent.RunCheck(ctx, url, "") printCheck(report) if !report.Coordinator.OK || !report.Python.OK || !report.Scimesh.OK { os.Exit(1) diff --git a/coordinator/internal/agent/check.go b/coordinator/internal/agent/check.go index f26568e..7ca74e4 100644 --- a/coordinator/internal/agent/check.go +++ b/coordinator/internal/agent/check.go @@ -70,17 +70,23 @@ func CheckCoordinator(ctx context.Context, url string, timeout time.Duration) Ch return report } -// CheckEnvironment verifies the local runtime: Python present and the scimesh -// package importable. +// CheckEnvironment verifies the local runtime against the python3 found on +// PATH. func CheckEnvironment(ctx context.Context) CheckReport { - report := CheckReport{Agent: Version} python, err := exec.LookPath("python3") if err != nil { - report.Python = CheckItem{Name: "python", OK: false, Detail: "python3 not found on PATH"} - return report + return CheckReport{Agent: Version, Python: CheckItem{Name: "python", OK: false, Detail: "python3 not found on PATH"}} } - report.Python = CheckItem{Name: "python", OK: true, Detail: python} - //nolint:gosec // G204: python comes from LookPath, the argument list is constant + return CheckEnvironmentWithPython(ctx, python) +} + +// CheckEnvironmentWithPython verifies the local runtime against a specific +// interpreter — the wizard's managed venv python when the runtime installer +// has created one, so the preflight reflects what the worker will actually +// execute with. +func CheckEnvironmentWithPython(ctx context.Context, python string) CheckReport { + report := CheckReport{Agent: Version, Python: CheckItem{Name: "python", OK: true, Detail: python}} + //nolint:gosec // G204: python is a resolved interpreter path, the argument list is constant cmd := exec.CommandContext(ctx, python, "-c", "import scimesh; print(scimesh.__version__ if hasattr(scimesh, '__version__') else 'installed')") out, err := cmd.Output() if err != nil { @@ -96,10 +102,17 @@ func CheckEnvironment(ctx context.Context) CheckReport { } // RunCheck combines the coordinator probe and the local environment probe; it -// is the body behind `worker-agent --check` and the wizard's test step. -func RunCheck(ctx context.Context, coordinatorURL string) CheckReport { +// is the body behind `worker-agent --check` and the wizard's test step. A +// non-empty python overrides the interpreter probed for the scimesh package +// (the managed venv after a runtime install). +func RunCheck(ctx context.Context, coordinatorURL, python string) CheckReport { report := CheckCoordinator(ctx, coordinatorURL, 15*time.Second) - env := CheckEnvironment(ctx) + var env CheckReport + if python != "" { + env = CheckEnvironmentWithPython(ctx, python) + } else { + env = CheckEnvironment(ctx) + } report.Python = env.Python report.Scimesh = env.Scimesh return report diff --git a/coordinator/internal/agent/setupui/server.go b/coordinator/internal/agent/setupui/server.go index 16becaf..57e74c5 100644 --- a/coordinator/internal/agent/setupui/server.go +++ b/coordinator/internal/agent/setupui/server.go @@ -411,7 +411,10 @@ func (s *Server) handleTest(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "coordinator_url is required"}) return } - report := agent.RunCheck(r.Context(), url) + // After the runtime installer created the venv, probe that interpreter: + // checking the bare system python3 would keep reporting scimesh as + // missing even though the worker would run with the venv. + report := agent.RunCheck(r.Context(), url, s.venvPython()) writeJSON(w, http.StatusOK, report) } diff --git a/coordinator/internal/agent/setupui/server_test.go b/coordinator/internal/agent/setupui/server_test.go index ab617f6..f86b020 100644 --- a/coordinator/internal/agent/setupui/server_test.go +++ b/coordinator/internal/agent/setupui/server_test.go @@ -454,3 +454,28 @@ func TestSaveConfigPinsVenvRunnerWhenPresent(t *testing.T) { t.Errorf("task runner = %v, want the venv python", config.TaskRunner) } } + +func TestTestProbesTheVenvPythonAfterInstall(t *testing.T) { + sup := &fakeSup{} + server, base := newTestServer(t, sup) + // The runtime installer leaves a venv python; make it a stub that reports + // a fake scimesh version so the preflight goes green through the venv. + venvPython := filepath.Join(server.dir, "venv", "bin", "python") + _ = os.MkdirAll(filepath.Dir(venvPython), 0o755) + _ = os.WriteFile(venvPython, []byte("#!/bin/sh\nif [ \"$1\" = \"-c\" ]; then echo 9.9.9-test; exit 0; fi\nexit 0\n"), 0o755) + + req, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, base+"/api/test", strings.NewReader(`{"coordinator_url":"http://127.0.0.1:1"}`)) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + defer func() { _ = resp.Body.Close() }() + var report agent.CheckReport + if err := json.NewDecoder(resp.Body).Decode(&report); err != nil { + t.Fatal(err) + } + if !report.Scimesh.OK || report.Scimesh.Detail != "9.9.9-test" { + t.Errorf("scimesh check = %+v, want the venv interpreter reporting 9.9.9-test", report.Scimesh) + } +} diff --git a/mkdocs/standalone.md b/mkdocs/standalone.md index 555f0b8..6f3f0e5 100644 --- a/mkdocs/standalone.md +++ b/mkdocs/standalone.md @@ -59,6 +59,13 @@ curl -fsSL https://raw.githubusercontent.com/emil28092005/SciMesh/main/install.s # the installer opens the local wizard at http://127.0.0.1:12700 automatically ``` +On a headless server (no desktop environment), RDKit needs a few X11 +libraries that desktops already ship — install them once with apt: + +```bash +sudo apt-get install -y libxrender1 libxext6 libxcursor1 libxfixes3 libxi6 libxrandr2 +``` + Or configure by hand: ```bash