From 8278cef5c7d2ca8f1a32a8aa841250a630dd9b9c Mon Sep 17 00:00:00 2001 From: T-Crypt <132922589+T-Crypt@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:51:21 -0600 Subject: [PATCH] fix(runner): keep the engine in a Type=notify service's cgroup systemd accepts sd_notify only from the service's own cgroup, so since the cgroup scopes an engine that signals readiness from its new scope left the unit in "activating". Inside such a service (NOTIFY_SOCKET set) the run now starts the engine without a scope, as before. Measured with the built binary as the main command of a Type=notify user service: activating before, active after. --- CHANGELOG.md | 5 +++-- internal/runner/runner_unix.go | 13 +++++++++++++ internal/runner/scope_unix_test.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 internal/runner/scope_unix_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index cc4d2d1..3fd4dde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,8 +26,9 @@ has a matching GitHub release whose body comes from its section below. turns stuck naming it, `berth reclaim` lists it, and a whole-card request is never granted `contested` over it. A GPU process in a scope no live lease recorded is a stowaway as before, and a process outside every scope is never attributed to a - scopeless lease. Without a user manager, and on Windows, the run starts as before - and the window stands. + scopeless lease. Without a user manager, inside a `Type=notify` service (where + systemd only accepts readiness from the service's own cgroup), and on Windows, the run + starts as before and the window stands. ## [v0.3.3] - 2026-10-21 diff --git a/internal/runner/runner_unix.go b/internal/runner/runner_unix.go index ba3aa20..0a0a9b5 100644 --- a/internal/runner/runner_unix.go +++ b/internal/runner/runner_unix.go @@ -40,6 +40,12 @@ func scopeToChild(as string, argv []string, log *slog.Logger) []string { if testing.Testing() { return argv } + if !scopeAllowed(os.Getenv) { + if log != nil { + log.Debug("inside a Type=notify service; the engine stays in its cgroup", "engine", as) + } + return argv + } if !sysd.ScopeReady() { if log != nil { log.Debug("no systemd user manager; the engine runs in this run's cgroup", "engine", as) @@ -49,6 +55,13 @@ func scopeToChild(as string, argv []string, log *slog.Logger) []string { return sysd.ScopeArgv("berth-run", as, argv) } +// scopeAllowed is false inside a Type=notify service (NOTIFY_SOCKET set): +// systemd drops a readiness message sent from outside the service's +// cgroup, so the unit would never leave "activating". +func scopeAllowed(getenv func(string) string) bool { + return getenv("NOTIFY_SOCKET") == "" +} + func signalGroup(cmd *exec.Cmd, s os.Signal) { if sig, ok := s.(syscall.Signal); ok { _ = syscall.Kill(-cmd.Process.Pid, sig) diff --git a/internal/runner/scope_unix_test.go b/internal/runner/scope_unix_test.go new file mode 100644 index 0000000..4656f01 --- /dev/null +++ b/internal/runner/scope_unix_test.go @@ -0,0 +1,29 @@ +// Copyright 2026 Trevin Tindall (T-Crypt) +// SPDX-License-Identifier: Apache-2.0 + +//go:build unix + +package runner + +import "testing" + +func TestScopeAllowed(t *testing.T) { + for _, tc := range []struct { + notify string + want bool + }{ + {"", true}, + {"/run/user/1000/systemd/notify", false}, + {"@/org/freedesktop/systemd1/notify/123", false}, + } { + env := func(k string) string { + if k == "NOTIFY_SOCKET" { + return tc.notify + } + return "" + } + if got := scopeAllowed(env); got != tc.want { + t.Errorf("NOTIFY_SOCKET=%q: scopeAllowed = %v, want %v", tc.notify, got, tc.want) + } + } +}