From 2874ef3bfcd150dedf2b683542dbc59cf9478b62 Mon Sep 17 00:00:00 2001 From: jakub961241 <144362244+jakub961241@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:38:40 +0100 Subject: [PATCH 1/2] fix: resolve bash not found on minimal OS installs (#12257) On minimal OS installations (e.g., AlmaLinux 10), bash may not be in the default $PATH, causing snapshot restore and other operations to fail with: exec: "bash": executable file not found in $PATH Add BashPath() helper that uses exec.LookPath first, then falls back to checking /bin/bash, /usr/bin/bash, /usr/local/bin/bash. Applied to both core and agent cmd packages and all callers. Fixes #12257 --- agent/utils/cmd/cmd.go | 26 ++++++++++++++++++++++++++ agent/utils/cmd/cmdx.go | 12 ++++++------ core/i18n/i18n.go | 3 ++- core/utils/cmd/cmd.go | 29 ++++++++++++++++++++++++++++- core/utils/cmd/cmdx.go | 12 ++++++------ core/utils/terminal/local_cmd.go | 3 ++- 6 files changed, 70 insertions(+), 15 deletions(-) diff --git a/agent/utils/cmd/cmd.go b/agent/utils/cmd/cmd.go index fd1d7f46a6bf..e05e84165fb3 100644 --- a/agent/utils/cmd/cmd.go +++ b/agent/utils/cmd/cmd.go @@ -1,10 +1,36 @@ package cmd import ( + "os" "os/exec" "strings" + "sync" ) +var ( + bashPathOnce sync.Once + bashPathCache string +) + +// BashPath returns the full path to bash, falling back to common locations +// if "bash" is not found in $PATH. +func BashPath() string { + bashPathOnce.Do(func() { + if p, err := exec.LookPath("bash"); err == nil { + bashPathCache = p + return + } + for _, candidate := range []string{"/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash"} { + if _, err := os.Stat(candidate); err == nil { + bashPathCache = candidate + return + } + } + bashPathCache = "bash" + }) + return bashPathCache +} + func CheckIllegal(args ...string) bool { if args == nil { return false diff --git a/agent/utils/cmd/cmdx.go b/agent/utils/cmd/cmdx.go index 27689dce22f6..1a4ff2d9cf70 100644 --- a/agent/utils/cmd/cmdx.go +++ b/agent/utils/cmd/cmdx.go @@ -65,18 +65,18 @@ func (c *CommandHelper) Run(name string, arg ...string) error { } func (c *CommandHelper) RunBashCWithArgs(arg ...string) error { arg = append([]string{"-c"}, arg...) - _, err := c.run("bash", arg...) + _, err := c.run(BashPath(), arg...) return err } func (c *CommandHelper) RunBashC(command string) error { - if _, err := c.run("bash", "-c", command); err != nil { + if _, err := c.run(BashPath(), "-c", command); err != nil { return err } return nil } func (c *CommandHelper) RunBashCf(command string, arg ...interface{}) error { - if _, err := c.run("bash", "-c", fmt.Sprintf(command, arg...)); err != nil { + if _, err := c.run(BashPath(), "-c", fmt.Sprintf(command, arg...)); err != nil { return err } return nil @@ -86,10 +86,10 @@ func (c *CommandHelper) RunWithStdout(name string, arg ...string) (string, error return c.run(name, arg...) } func (c *CommandHelper) RunWithStdoutBashC(command string) (string, error) { - return c.run("bash", "-c", command) + return c.run(BashPath(), "-c", command) } func (c *CommandHelper) RunWithStdoutBashCf(command string, arg ...interface{}) (string, error) { - return c.run("bash", "-c", fmt.Sprintf(command, arg...)) + return c.run(BashPath(), "-c", fmt.Sprintf(command, arg...)) } func (c *CommandHelper) run(name string, arg ...string) (string, error) { @@ -136,7 +136,7 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) { cmd.Stdout = file cmd.Stderr = file } else if len(c.scriptPath) != 0 { - cmd = exec.Command("bash", c.scriptPath) + cmd = exec.Command(BashPath(), c.scriptPath) cmd.Stdout = &stdout cmd.Stderr = &stderr } else { diff --git a/core/i18n/i18n.go b/core/i18n/i18n.go index b80f58ebde83..9373e730f9ab 100644 --- a/core/i18n/i18n.go +++ b/core/i18n/i18n.go @@ -8,6 +8,7 @@ import ( "sync/atomic" "github.com/1Panel-dev/1Panel/core/app/repo" + cmd2 "github.com/1Panel-dev/1Panel/core/utils/cmd" "github.com/1Panel-dev/1Panel/core/global" "github.com/gin-gonic/gin" "github.com/nicksnyder/go-i18n/v2/i18n" @@ -209,7 +210,7 @@ func getLanguageFromDBInternal() string { return lang } func getLanguageFrom1pctl() string { - cmd := exec.Command("bash", "-c", "grep '^LANGUAGE=' /usr/local/bin/1pctl | cut -d'=' -f2") + cmd := exec.Command(cmd2.BashPath(), "-c", "grep '^LANGUAGE=' /usr/local/bin/1pctl | cut -d'=' -f2") stdout, err := cmd.CombinedOutput() if err != nil { panic(err) diff --git a/core/utils/cmd/cmd.go b/core/utils/cmd/cmd.go index 49a7b32010e2..5f4a4e6a74c9 100644 --- a/core/utils/cmd/cmd.go +++ b/core/utils/cmd/cmd.go @@ -4,10 +4,37 @@ import ( "bufio" "fmt" "io" + "os" "os/exec" "strings" + "sync" ) +var ( + bashPathOnce sync.Once + bashPathCache string +) + +// BashPath returns the full path to bash, falling back to common locations +// if "bash" is not found in $PATH. This fixes issues on minimal OS installs +// (e.g., AlmaLinux) where bash may not be in the default PATH. +func BashPath() string { + bashPathOnce.Do(func() { + if p, err := exec.LookPath("bash"); err == nil { + bashPathCache = p + return + } + for _, candidate := range []string{"/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash"} { + if _, err := os.Stat(candidate); err == nil { + bashPathCache = candidate + return + } + } + bashPathCache = "bash" + }) + return bashPathCache +} + func SudoHandleCmd() string { cmd := exec.Command("sudo", "-n", "ls") if err := cmd.Run(); err == nil { @@ -25,7 +52,7 @@ func Which(name string) bool { } func ExecWithStreamOutput(command string, outputCallback func(string)) error { - cmd := exec.Command("bash", "-c", command) + cmd := exec.Command(BashPath(), "-c", command) stdout, err := cmd.StdoutPipe() if err != nil { diff --git a/core/utils/cmd/cmdx.go b/core/utils/cmd/cmdx.go index 71be68afa453..2b3b9f849dd0 100644 --- a/core/utils/cmd/cmdx.go +++ b/core/utils/cmd/cmdx.go @@ -60,15 +60,15 @@ func (c *CommandHelper) Run(name string, arg ...string) error { } func (c *CommandHelper) RunBashCWithArgs(arg ...string) error { arg = append([]string{"-c"}, arg...) - _, err := c.run("bash", arg...) + _, err := c.run(BashPath(), arg...) return err } func (c *CommandHelper) RunBashC(command string) error { - _, err := c.run("bash", "-c", command) + _, err := c.run(BashPath(), "-c", command) return err } func (c *CommandHelper) RunBashCf(command string, arg ...interface{}) error { - _, err := c.run("bash", "-c", fmt.Sprintf(command, arg...)) + _, err := c.run(BashPath(), "-c", fmt.Sprintf(command, arg...)) return err } @@ -76,10 +76,10 @@ func (c *CommandHelper) RunWithStdout(name string, arg ...string) (string, error return c.run(name, arg...) } func (c *CommandHelper) RunWithStdoutBashC(command string) (string, error) { - return c.run("bash", "-c", command) + return c.run(BashPath(), "-c", command) } func (c *CommandHelper) RunWithStdoutBashCf(command string, arg ...interface{}) (string, error) { - return c.run("bash", "-c", fmt.Sprintf(command, arg...)) + return c.run(BashPath(), "-c", fmt.Sprintf(command, arg...)) } func (c *CommandHelper) run(name string, arg ...string) (string, error) { @@ -114,7 +114,7 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) { } else if len(c.scriptPath) != 0 { cmd.Stdout = &stdout cmd.Stderr = &stderr - cmd = exec.Command("bash", c.scriptPath) + cmd = exec.Command(BashPath(), c.scriptPath) } else { cmd.Stdout = &stdout cmd.Stderr = &stderr diff --git a/core/utils/terminal/local_cmd.go b/core/utils/terminal/local_cmd.go index 0ef1ca3dc787..e3e9ece47705 100644 --- a/core/utils/terminal/local_cmd.go +++ b/core/utils/terminal/local_cmd.go @@ -7,6 +7,7 @@ import ( "time" "unsafe" + cmd2 "github.com/1Panel-dev/1Panel/core/utils/cmd" "github.com/1Panel-dev/1Panel/core/global" "github.com/creack/pty" "github.com/pkg/errors" @@ -26,7 +27,7 @@ type LocalCommand struct { } func NewCommand(script string) (*LocalCommand, error) { - cmd := exec.Command("bash") + cmd := exec.Command(cmd2.BashPath()) if term := os.Getenv("TERM"); term != "" { cmd.Env = append(os.Environ(), "TERM="+term) } else { From a3199b319be09be4d00c9dc761eefac30ac0b08b Mon Sep 17 00:00:00 2001 From: jakub961241 <144362244+jakub961241@users.noreply.github.com> Date: Sun, 22 Mar 2026 12:04:51 +0100 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20resolve=20import=20cycle=20in=20core?= =?UTF-8?q?/i18n=20=E2=86=92=20core/utils/cmd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit i18n.go imported cmd package which created a cycle: i18n → cmd → task → buserr → i18n Fix: inline bash path lookup in i18n.go instead of calling cmd.BashPath(). Use cmdutil alias in terminal/local_cmd.go to avoid variable name conflict. --- core/i18n/i18n.go | 15 +++++++++++++-- core/utils/terminal/local_cmd.go | 4 ++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/core/i18n/i18n.go b/core/i18n/i18n.go index 9373e730f9ab..227858c36abf 100644 --- a/core/i18n/i18n.go +++ b/core/i18n/i18n.go @@ -3,12 +3,12 @@ package i18n import ( "embed" "fmt" + "os" "os/exec" "strings" "sync/atomic" "github.com/1Panel-dev/1Panel/core/app/repo" - cmd2 "github.com/1Panel-dev/1Panel/core/utils/cmd" "github.com/1Panel-dev/1Panel/core/global" "github.com/gin-gonic/gin" "github.com/nicksnyder/go-i18n/v2/i18n" @@ -210,7 +210,18 @@ func getLanguageFromDBInternal() string { return lang } func getLanguageFrom1pctl() string { - cmd := exec.Command(cmd2.BashPath(), "-c", "grep '^LANGUAGE=' /usr/local/bin/1pctl | cut -d'=' -f2") + bashBin := "bash" + if p, err := exec.LookPath("bash"); err == nil { + bashBin = p + } else { + for _, candidate := range []string{"/bin/bash", "/usr/bin/bash"} { + if _, err := os.Stat(candidate); err == nil { + bashBin = candidate + break + } + } + } + cmd := exec.Command(bashBin, "-c", "grep '^LANGUAGE=' /usr/local/bin/1pctl | cut -d'=' -f2") stdout, err := cmd.CombinedOutput() if err != nil { panic(err) diff --git a/core/utils/terminal/local_cmd.go b/core/utils/terminal/local_cmd.go index e3e9ece47705..319a7801651b 100644 --- a/core/utils/terminal/local_cmd.go +++ b/core/utils/terminal/local_cmd.go @@ -7,7 +7,7 @@ import ( "time" "unsafe" - cmd2 "github.com/1Panel-dev/1Panel/core/utils/cmd" + cmdutil "github.com/1Panel-dev/1Panel/core/utils/cmd" "github.com/1Panel-dev/1Panel/core/global" "github.com/creack/pty" "github.com/pkg/errors" @@ -27,7 +27,7 @@ type LocalCommand struct { } func NewCommand(script string) (*LocalCommand, error) { - cmd := exec.Command(cmd2.BashPath()) + cmd := exec.Command(cmdutil.BashPath()) if term := os.Getenv("TERM"); term != "" { cmd.Env = append(os.Environ(), "TERM="+term) } else {