On MacOS allow UserShell to override /bin/bash (#137)

* switch emain to using the bash from PATH, not defaulting to /bin/bash.  not convinced this fixes #132 yet because the PATH set for MacOS applications is not set from .bash_profile or .zprofile.  those seem to be set with launchctl setenv.

* move MacUserShell() func to shexec.  use dscl UserShell output as bash path on MacOS when present.  fixes #132
This commit is contained in:
Mike Sawka 2023-12-14 21:45:27 -08:00 committed by GitHub
parent 3b65e9941a
commit 8200a312b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 60 additions and 38 deletions

View File

@ -547,7 +547,7 @@ function runWaveSrv() {
envCopy[WaveDevVarName] = "1";
}
console.log("trying to run local server", getWaveSrvPath());
let proc = child_process.spawn("/bin/bash", ["-c", getWaveSrvCmd()], {
let proc = child_process.spawn("bash", ["-c", getWaveSrvCmd()], {
cwd: getWaveSrvCwd(),
env: envCopy,
});

View File

@ -150,7 +150,7 @@ func runSingleCompGen(cwd string, compType string, prefix string) ([]string, boo
return nil, false, fmt.Errorf("invalid compgen type '%s'", compType)
}
compGenCmdStr := fmt.Sprintf("cd %s; compgen -A %s -- %s | sort | uniq | head -n %d", shellescape.Quote(cwd), shellescape.Quote(compType), shellescape.Quote(prefix), packet.MaxCompGenValues+1)
ecmd := exec.Command("bash", "-c", compGenCmdStr)
ecmd := exec.Command(shexec.GetLocalBashPath(), "-c", compGenCmdStr)
outputBytes, err := ecmd.Output()
if err != nil {
return nil, false, fmt.Errorf("compgen error: %w", err)

View File

@ -14,6 +14,7 @@ import (
"os/signal"
"os/user"
"path"
"regexp"
"runtime"
"strconv"
"strings"
@ -52,6 +53,8 @@ const RtnStateFdNum = 20
const ReturnStateReadWaitTime = 2 * time.Second
const GetStateTimeout = 5 * time.Second
const RemoteBashPath = "bash"
const DefaultMacOSShell = "/bin/bash"
const BaseBashOpts = `set +m; set +H; shopt -s extglob`
@ -62,7 +65,7 @@ var LocalBashMajorVersionOnce = &sync.Once{}
var LocalBashMajorVersion = ""
var GetShellStateCmds = []string{
`echo bash v${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}.${BASH_VERSINFO[2]};`,
ShellVersionCmdStr + ";",
`pwd;`,
`declare -p $(compgen -A variable);`,
`alias -p;`,
@ -101,6 +104,7 @@ func MakeInstallCommandStr() string {
return strings.ReplaceAll(InstallCommandFmt, "[%VERSION%]", semver.MajorMinor(base.MShellVersion))
}
// TODO fix bash path in these constants
const RunCommandFmt = `%s`
const RunSudoCommandFmt = `sudo -n -C %d bash /dev/fd/%d`
const RunSudoPasswordCommandFmt = `cat /dev/fd/%d | sudo -k -S -C %d bash -c "echo '[from-mshell]'; exec %d>&-; bash /dev/fd/%d < /dev/fd/%d"`
@ -179,6 +183,16 @@ type ShExecUPR struct {
UPR packet.UnknownPacketReporter
}
func GetLocalBashPath() string {
if runtime.GOOS == "darwin" {
macShell := GetMacUserShell()
if strings.Index(macShell, "bash") != -1 {
return shellescape.Quote(macShell)
}
}
return "bash"
}
func GetShellStateCmd() string {
return strings.Join(GetShellStateCmds, ` printf "\x00\x00";`)
}
@ -320,7 +334,7 @@ func MakeDetachedExecCmd(pk *packet.RunPacketType, cmdTty *os.File) (*exec.Cmd,
if state == nil {
state = &packet.ShellState{}
}
ecmd := exec.Command("bash", "-c", pk.Command)
ecmd := exec.Command(GetLocalBashPath(), "-c", pk.Command)
if !pk.StateComplete {
ecmd.Env = os.Environ()
}
@ -525,7 +539,7 @@ func (opts SSHOpts) MakeSSHExecCmd(remoteCommand string) *exec.Cmd {
if homeDir == "" {
homeDir = "/"
}
ecmd := exec.Command("bash", "-c", remoteCommand)
ecmd := exec.Command(GetLocalBashPath(), "-c", remoteCommand)
ecmd.Dir = homeDir
return ecmd
} else {
@ -552,7 +566,7 @@ func (opts SSHOpts) MakeSSHExecCmd(remoteCommand string) *exec.Cmd {
}
// note that SSHOptsStr is *not* escaped
sshCmd := fmt.Sprintf("ssh %s %s %s %s", strings.Join(moreSSHOpts, " "), opts.SSHOptsStr, shellescape.Quote(opts.SSHHost), shellescape.Quote(remoteCommand))
ecmd := exec.Command("bash", "-c", sshCmd)
ecmd := exec.Command(RemoteBashPath, "-c", sshCmd)
return ecmd
}
}
@ -1141,9 +1155,9 @@ func RunCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender, fro
rcFileName = fmt.Sprintf("/dev/fd/%d", rcFileFdNum)
}
if pk.UsePty {
cmd.Cmd = exec.Command("bash", "--rcfile", rcFileName, "-i", "-c", pk.Command)
cmd.Cmd = exec.Command(GetLocalBashPath(), "--rcfile", rcFileName, "-i", "-c", pk.Command)
} else {
cmd.Cmd = exec.Command("bash", "--rcfile", rcFileName, "-c", pk.Command)
cmd.Cmd = exec.Command(GetLocalBashPath(), "--rcfile", rcFileName, "-c", pk.Command)
}
if !pk.StateComplete {
cmd.Cmd.Env = os.Environ()
@ -1582,7 +1596,7 @@ func GetShellState() (*packet.ShellState, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), GetStateTimeout)
defer cancelFn()
cmdStr := BaseBashOpts + "; " + GetShellStateCmd()
ecmd := exec.CommandContext(ctx, "bash", "-l", "-i", "-c", cmdStr)
ecmd := exec.CommandContext(ctx, GetLocalBashPath(), "-l", "-i", "-c", cmdStr)
outputBytes, err := runSimpleCmdInPty(ecmd)
if err != nil {
return nil, err
@ -1625,3 +1639,40 @@ func GetLocalBashMajorVersion() string {
})
return LocalBashMajorVersion
}
var userShellRegexp = regexp.MustCompile(`^UserShell: (.*)$`)
var cachedMacUserShell string
var macUserShellOnce = &sync.Once{}
func GetMacUserShell() string {
if runtime.GOOS != "darwin" {
return ""
}
macUserShellOnce.Do(func() {
cachedMacUserShell = internalMacUserShell()
})
return cachedMacUserShell
}
// dscl . -read /User/[username] UserShell
// defaults to /bin/bash
func internalMacUserShell() string {
osUser, err := user.Current()
if err != nil {
return DefaultMacOSShell
}
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn()
userStr := "/Users/" + osUser.Username
out, err := exec.CommandContext(ctx, "dscl", ".", "-read", userStr, "UserShell").CombinedOutput()
if err != nil {
return DefaultMacOSShell
}
outStr := strings.TrimSpace(string(out))
m := userShellRegexp.FindStringSubmatch(outStr)
if m == nil {
return DefaultMacOSShell
}
return m[1]
}

View File

@ -12,7 +12,6 @@ import (
"log"
"os"
"os/exec"
"os/user"
"path"
"regexp"
"runtime"
@ -39,7 +38,6 @@ const WaveAppPathVarName = "WAVETERM_APP_PATH"
const WaveVersion = "v0.5.1"
const WaveAuthKeyFileName = "waveterm.authkey"
const MShellVersion = "v0.3.0"
const DefaultMacOSShell = "/bin/bash"
var SessionDirCache = make(map[string]string)
var ScreenDirCache = make(map[string]string)
@ -379,30 +377,3 @@ func MacOSRelease() string {
})
return osRelease
}
var userShellRegexp = regexp.MustCompile(`^UserShell: (.*)$`)
// dscl . -read /User/[username] UserShell
// defaults to /bin/bash
func MacUserShell() string {
osUser, err := user.Current()
if err != nil {
log.Printf("error getting current user: %v\n", err)
return DefaultMacOSShell
}
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn()
userStr := "/Users/" + osUser.Name
out, err := exec.CommandContext(ctx, "dscl", ".", "-read", userStr, "UserShell").CombinedOutput()
if err != nil {
log.Printf("error executing macos user shell lookup: %v %q\n", err, string(out))
return DefaultMacOSShell
}
outStr := strings.TrimSpace(string(out))
m := userShellRegexp.FindStringSubmatch(outStr)
if m == nil {
log.Printf("error in format of dscl output: %q\n", outStr)
return DefaultMacOSShell
}
return m[1]
}