From b06bc0a4554f9e9bf816b0f9a2ecac3b857bcaec Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Tue, 5 May 2026 19:47:02 -0700 Subject: [PATCH] fix(cmd): use exec.LookPath in Which() so detection works without external 'which' (#12651) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cmd.Which() previously shelled out to `which ` to determine whether a binary was on PATH. On distributions that do not ship a `which` package by default — Arch Linux is the canonical example, but the same applies to several minimal container images — `which` itself is missing, so every call to Which() returned false and 1Panel reported core dependencies (notably Docker) as 'not installed' even when they were running normally. Switch the primary path to Go's exec.LookPath, which uses the process PATH directly and has no external dependency. The original shell-out is preserved as a fallback so any environment where the agent's PATH differs from the user's interactive shell PATH (the original reason the shell-out existed) keeps working unchanged. Both copies are updated (agent/utils/cmd/cmd.go and core/utils/cmd/cmd.go) and a small unit test is added to each package to guard the regression. Fixes #12605 Signed-off-by: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> --- agent/utils/cmd/cmd.go | 10 ++++++++++ agent/utils/cmd/cmd_test.go | 23 +++++++++++++++++++++++ core/utils/cmd/cmd.go | 10 ++++++++++ core/utils/cmd/cmd_test.go | 23 +++++++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 agent/utils/cmd/cmd_test.go create mode 100644 core/utils/cmd/cmd_test.go diff --git a/agent/utils/cmd/cmd.go b/agent/utils/cmd/cmd.go index fd1d7f46a..5043923ef 100644 --- a/agent/utils/cmd/cmd.go +++ b/agent/utils/cmd/cmd.go @@ -29,6 +29,16 @@ func SudoHandleCmd() string { } func Which(name string) bool { + // Prefer Go's built-in PATH lookup so we don't depend on an external + // `which` binary, which is not installed by default on minimal + // distributions (e.g. Arch Linux, some Alpine images, slim containers). + // See 1Panel-dev/1Panel#12605. + if _, err := exec.LookPath(name); err == nil { + return true + } + // Fall back to shelling out for environments where PATH inside the + // agent process differs from the user's interactive shell PATH (the + // previous behaviour, preserved for compatibility). stdout, err := RunDefaultWithStdoutBashCf("which %s", name) if err != nil || (len(strings.ReplaceAll(stdout, "\n", "")) == 0) { return false diff --git a/agent/utils/cmd/cmd_test.go b/agent/utils/cmd/cmd_test.go new file mode 100644 index 000000000..d2c622af5 --- /dev/null +++ b/agent/utils/cmd/cmd_test.go @@ -0,0 +1,23 @@ +package cmd + +import "testing" + +// TestWhich_ExistingBinary verifies that Which() returns true for a +// binary that is guaranteed to exist on every Unix-like build host +// (`sh`). Regression test for #12605: the previous implementation +// shelled out to `which`, which is not always available on minimal +// distributions like Arch Linux. The new implementation tries +// exec.LookPath first, so this assertion holds regardless of whether +// `which` itself is on PATH. +func TestWhich_ExistingBinary(t *testing.T) { + if !Which("sh") { + t.Errorf("Which(\"sh\") = false, want true") + } +} + +func TestWhich_MissingBinary(t *testing.T) { + // A binary name that is extremely unlikely to exist on any host. + if Which("definitely-not-a-real-binary-xyzzy-1panel") { + t.Errorf("Which(\"definitely-not-a-real-binary-xyzzy-1panel\") = true, want false") + } +} diff --git a/core/utils/cmd/cmd.go b/core/utils/cmd/cmd.go index 49a7b3201..4bf577c51 100644 --- a/core/utils/cmd/cmd.go +++ b/core/utils/cmd/cmd.go @@ -17,6 +17,16 @@ func SudoHandleCmd() string { } func Which(name string) bool { + // Prefer Go's built-in PATH lookup so we don't depend on an external + // `which` binary, which is not installed by default on minimal + // distributions (e.g. Arch Linux, some Alpine images, slim containers). + // See 1Panel-dev/1Panel#12605. + if _, err := exec.LookPath(name); err == nil { + return true + } + // Fall back to shelling out for environments where PATH inside the + // agent process differs from the user's interactive shell PATH (the + // previous behaviour, preserved for compatibility). stdout, err := RunDefaultWithStdoutBashCf("which %s", name) if err != nil || (len(strings.ReplaceAll(stdout, "\n", "")) == 0) { return false diff --git a/core/utils/cmd/cmd_test.go b/core/utils/cmd/cmd_test.go new file mode 100644 index 000000000..d2c622af5 --- /dev/null +++ b/core/utils/cmd/cmd_test.go @@ -0,0 +1,23 @@ +package cmd + +import "testing" + +// TestWhich_ExistingBinary verifies that Which() returns true for a +// binary that is guaranteed to exist on every Unix-like build host +// (`sh`). Regression test for #12605: the previous implementation +// shelled out to `which`, which is not always available on minimal +// distributions like Arch Linux. The new implementation tries +// exec.LookPath first, so this assertion holds regardless of whether +// `which` itself is on PATH. +func TestWhich_ExistingBinary(t *testing.T) { + if !Which("sh") { + t.Errorf("Which(\"sh\") = false, want true") + } +} + +func TestWhich_MissingBinary(t *testing.T) { + // A binary name that is extremely unlikely to exist on any host. + if Which("definitely-not-a-real-binary-xyzzy-1panel") { + t.Errorf("Which(\"definitely-not-a-real-binary-xyzzy-1panel\") = true, want false") + } +}