waveterm/pkg/shellexec/shellexec.go
Mike Sawka bba94a62d0
add jwt token back to wsl connections (#1841)
Co-authored-by: Evan Simkowitz <esimkowitz@users.noreply.github.com>
2025-01-24 14:24:15 -08:00

644 lines
22 KiB
Go

// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package shellexec
import (
"bytes"
"context"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/creack/pty"
"github.com/wavetermdev/waveterm/pkg/blocklogger"
"github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
"github.com/wavetermdev/waveterm/pkg/util/pamparse"
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
"github.com/wavetermdev/waveterm/pkg/wshutil"
"github.com/wavetermdev/waveterm/pkg/wslconn"
)
const DefaultGracefulKillWait = 400 * time.Millisecond
type CommandOptsType struct {
Interactive bool `json:"interactive,omitempty"`
Login bool `json:"login,omitempty"`
Cwd string `json:"cwd,omitempty"`
ShellPath string `json:"shellPath,omitempty"`
ShellOpts []string `json:"shellOpts,omitempty"`
SwapToken *shellutil.TokenSwapEntry `json:"swapToken,omitempty"`
}
type ShellProc struct {
ConnName string
Cmd ConnInterface
CloseOnce *sync.Once
DoneCh chan any // closed after proc.Wait() returns
WaitErr error // WaitErr is synchronized by DoneCh (written before DoneCh is closed) and CloseOnce
}
func (sp *ShellProc) Close() {
sp.Cmd.KillGraceful(DefaultGracefulKillWait)
go func() {
defer func() {
panichandler.PanicHandler("ShellProc.Close", recover())
}()
waitErr := sp.Cmd.Wait()
sp.SetWaitErrorAndSignalDone(waitErr)
// windows cannot handle the pty being
// closed twice, so we let the pty
// close itself instead
if runtime.GOOS != "windows" {
sp.Cmd.Close()
}
}()
}
func (sp *ShellProc) SetWaitErrorAndSignalDone(waitErr error) {
sp.CloseOnce.Do(func() {
sp.WaitErr = waitErr
close(sp.DoneCh)
})
}
func (sp *ShellProc) Wait() error {
<-sp.DoneCh
return sp.WaitErr
}
// returns (done, waitError)
func (sp *ShellProc) WaitNB() (bool, error) {
select {
case <-sp.DoneCh:
return true, sp.WaitErr
default:
return false, nil
}
}
func ExitCodeFromWaitErr(err error) int {
if err == nil {
return 0
}
if exitErr, ok := err.(*exec.ExitError); ok {
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
return status.ExitStatus()
}
}
return -1
}
func checkCwd(cwd string) error {
if cwd == "" {
return fmt.Errorf("cwd is empty")
}
if _, err := os.Stat(cwd); err != nil {
return fmt.Errorf("error statting cwd %q: %w", cwd, err)
}
return nil
}
type PipePty struct {
remoteStdinWrite *os.File
remoteStdoutRead *os.File
}
func (pp *PipePty) Fd() uintptr {
return pp.remoteStdinWrite.Fd()
}
func (pp *PipePty) Name() string {
return "pipe-pty"
}
func (pp *PipePty) Read(p []byte) (n int, err error) {
return pp.remoteStdoutRead.Read(p)
}
func (pp *PipePty) Write(p []byte) (n int, err error) {
return pp.remoteStdinWrite.Write(p)
}
func (pp *PipePty) Close() error {
err1 := pp.remoteStdinWrite.Close()
err2 := pp.remoteStdoutRead.Close()
if err1 != nil {
return err1
}
return err2
}
func (pp *PipePty) WriteString(s string) (n int, err error) {
return pp.Write([]byte(s))
}
func StartWslShellProcNoWsh(ctx context.Context, termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *wslconn.WslConn) (*ShellProc, error) {
client := conn.GetClient()
conn.Infof(ctx, "WSL-NEWSESSION (StartWslShellProcNoWsh)")
ecmd := exec.Command("wsl.exe", "~", "-d", client.Name())
if termSize.Rows == 0 || termSize.Cols == 0 {
termSize.Rows = shellutil.DefaultTermRows
termSize.Cols = shellutil.DefaultTermCols
}
if termSize.Rows <= 0 || termSize.Cols <= 0 {
return nil, fmt.Errorf("invalid term size: %v", termSize)
}
cmdPty, err := pty.StartWithSize(ecmd, &pty.Winsize{Rows: uint16(termSize.Rows), Cols: uint16(termSize.Cols)})
if err != nil {
return nil, err
}
cmdWrap := MakeCmdWrap(ecmd, cmdPty)
return &ShellProc{Cmd: cmdWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
}
func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *wslconn.WslConn) (*ShellProc, error) {
client := conn.GetClient()
conn.Infof(ctx, "WSL-NEWSESSION (StartWslShellProc)")
connRoute := wshutil.MakeConnectionRouteId(conn.GetName())
rpcClient := wshclient.GetBareRpcClient()
remoteInfo, err := wshclient.RemoteGetInfoCommand(rpcClient, &wshrpc.RpcOpts{Route: connRoute, Timeout: 2000})
if err != nil {
return nil, fmt.Errorf("unable to obtain client info: %w", err)
}
log.Printf("client info collected: %+#v", remoteInfo)
var shellPath string
if cmdOpts.ShellPath != "" {
conn.Infof(ctx, "using shell path from command opts: %s\n", cmdOpts.ShellPath)
shellPath = cmdOpts.ShellPath
}
configShellPath := conn.GetConfigShellPath()
if shellPath == "" && configShellPath != "" {
conn.Infof(ctx, "using shell path from config (conn:shellpath): %s\n", configShellPath)
shellPath = configShellPath
}
if shellPath == "" && remoteInfo.Shell != "" {
conn.Infof(ctx, "using shell path detected on remote machine: %s\n", remoteInfo.Shell)
shellPath = remoteInfo.Shell
}
if shellPath == "" {
conn.Infof(ctx, "no shell path detected, using default (/bin/bash)\n")
shellPath = "/bin/bash"
}
var shellOpts []string
var cmdCombined string
log.Printf("detected shell %q for conn %q\n", shellPath, conn.GetName())
err = wshclient.RemoteInstallRcFilesCommand(rpcClient, &wshrpc.RpcOpts{Route: connRoute, Timeout: 2000})
if err != nil {
log.Printf("error installing rc files: %v", err)
return nil, err
}
shellOpts = append(shellOpts, cmdOpts.ShellOpts...)
shellType := shellutil.GetShellTypeFromShellPath(shellPath)
conn.Infof(ctx, "detected shell type: %s\n", shellType)
if cmdStr == "" {
/* transform command in order to inject environment vars */
if shellType == shellutil.ShellType_bash {
// add --rcfile
// cant set -l or -i with --rcfile
bashPath := fmt.Sprintf("~/.waveterm/%s/.bashrc", shellutil.BashIntegrationDir)
shellOpts = append(shellOpts, "--rcfile", bashPath)
} else if shellType == shellutil.ShellType_fish {
if cmdOpts.Login {
shellOpts = append(shellOpts, "-l")
}
// source the wave.fish file
waveFishPath := fmt.Sprintf("~/.waveterm/%s/wave.fish", shellutil.FishIntegrationDir)
carg := fmt.Sprintf(`"source %s"`, waveFishPath)
shellOpts = append(shellOpts, "-C", carg)
} else if shellType == shellutil.ShellType_pwsh {
pwshPath := fmt.Sprintf("~/.waveterm/%s/wavepwsh.ps1", shellutil.PwshIntegrationDir)
// powershell is weird about quoted path executables and requires an ampersand first
shellPath = "& " + shellPath
shellOpts = append(shellOpts, "-ExecutionPolicy", "Bypass", "-NoExit", "-File", pwshPath)
} else {
if cmdOpts.Login {
shellOpts = append(shellOpts, "-l")
}
if cmdOpts.Interactive {
shellOpts = append(shellOpts, "-i")
}
// zdotdir setting moved to after session is created
}
cmdCombined = fmt.Sprintf("%s %s", shellPath, strings.Join(shellOpts, " "))
} else {
// TODO check quoting of cmdStr
shellPath = cmdStr
shellOpts = append(shellOpts, "-c", cmdStr)
cmdCombined = fmt.Sprintf("%s %s", shellPath, strings.Join(shellOpts, " "))
}
conn.Infof(ctx, "starting shell, using command: %s\n", cmdCombined)
conn.Infof(ctx, "WSL-NEWSESSION (StartWslShellProc)\n")
if shellType == shellutil.ShellType_zsh {
zshDir := fmt.Sprintf("~/.waveterm/%s", shellutil.ZshIntegrationDir)
conn.Infof(ctx, "setting ZDOTDIR to %s\n", zshDir)
cmdCombined = fmt.Sprintf(`ZDOTDIR=%s %s`, zshDir, cmdCombined)
}
packedToken, err := cmdOpts.SwapToken.PackForClient()
if err != nil {
conn.Infof(ctx, "error packing swap token: %v", err)
} else {
conn.Debugf(ctx, "packed swaptoken %s\n", packedToken)
cmdCombined = fmt.Sprintf(`%s=%s %s`, wavebase.WaveSwapTokenVarName, packedToken, cmdCombined)
}
jwtToken := cmdOpts.SwapToken.Env[wavebase.WaveJwtTokenVarName]
if jwtToken != "" {
cmdCombined = fmt.Sprintf(`%s=%s %s`, wavebase.WaveJwtTokenVarName, jwtToken, cmdCombined)
}
log.Printf("full combined command: %s", cmdCombined)
ecmd := exec.Command("wsl.exe", "~", "-d", client.Name(), "--", "sh", "-c", cmdCombined)
if termSize.Rows == 0 || termSize.Cols == 0 {
termSize.Rows = shellutil.DefaultTermRows
termSize.Cols = shellutil.DefaultTermCols
}
if termSize.Rows <= 0 || termSize.Cols <= 0 {
return nil, fmt.Errorf("invalid term size: %v", termSize)
}
shellutil.AddTokenSwapEntry(cmdOpts.SwapToken)
cmdPty, err := pty.StartWithSize(ecmd, &pty.Winsize{Rows: uint16(termSize.Rows), Cols: uint16(termSize.Cols)})
if err != nil {
return nil, err
}
cmdWrap := MakeCmdWrap(ecmd, cmdPty)
return &ShellProc{Cmd: cmdWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
}
func StartRemoteShellProcNoWsh(ctx context.Context, termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) {
client := conn.GetClient()
conn.Infof(ctx, "SSH-NEWSESSION (StartRemoteShellProcNoWsh)")
session, err := client.NewSession()
if err != nil {
return nil, err
}
remoteStdinRead, remoteStdinWriteOurs, err := os.Pipe()
if err != nil {
return nil, err
}
remoteStdoutReadOurs, remoteStdoutWrite, err := os.Pipe()
if err != nil {
return nil, err
}
pipePty := &PipePty{
remoteStdinWrite: remoteStdinWriteOurs,
remoteStdoutRead: remoteStdoutReadOurs,
}
if termSize.Rows == 0 || termSize.Cols == 0 {
termSize.Rows = shellutil.DefaultTermRows
termSize.Cols = shellutil.DefaultTermCols
}
if termSize.Rows <= 0 || termSize.Cols <= 0 {
return nil, fmt.Errorf("invalid term size: %v", termSize)
}
session.Stdin = remoteStdinRead
session.Stdout = remoteStdoutWrite
session.Stderr = remoteStdoutWrite
session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil)
sessionWrap := MakeSessionWrap(session, "", pipePty)
err = session.Shell()
if err != nil {
pipePty.Close()
return nil, err
}
return &ShellProc{Cmd: sessionWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
}
func StartRemoteShellProc(ctx context.Context, logCtx context.Context, termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) {
client := conn.GetClient()
connRoute := wshutil.MakeConnectionRouteId(conn.GetName())
rpcClient := wshclient.GetBareRpcClient()
remoteInfo, err := wshclient.RemoteGetInfoCommand(rpcClient, &wshrpc.RpcOpts{Route: connRoute, Timeout: 2000})
if err != nil {
return nil, fmt.Errorf("unable to obtain client info: %w", err)
}
log.Printf("client info collected: %+#v", remoteInfo)
var shellPath string
if cmdOpts.ShellPath != "" {
conn.Infof(logCtx, "using shell path from command opts: %s\n", cmdOpts.ShellPath)
shellPath = cmdOpts.ShellPath
}
configShellPath := conn.GetConfigShellPath()
if shellPath == "" && configShellPath != "" {
conn.Infof(logCtx, "using shell path from config (conn:shellpath): %s\n", configShellPath)
shellPath = configShellPath
}
if shellPath == "" && remoteInfo.Shell != "" {
conn.Infof(logCtx, "using shell path detected on remote machine: %s\n", remoteInfo.Shell)
shellPath = remoteInfo.Shell
}
if shellPath == "" {
conn.Infof(logCtx, "no shell path detected, using default (/bin/bash)\n")
shellPath = "/bin/bash"
}
var shellOpts []string
var cmdCombined string
log.Printf("detected shell %q for conn %q\n", shellPath, conn.GetName())
shellOpts = append(shellOpts, cmdOpts.ShellOpts...)
shellType := shellutil.GetShellTypeFromShellPath(shellPath)
conn.Infof(logCtx, "detected shell type: %s\n", shellType)
conn.Infof(logCtx, "swaptoken: %s\n", cmdOpts.SwapToken.Token)
if cmdStr == "" {
/* transform command in order to inject environment vars */
if shellType == shellutil.ShellType_bash {
// add --rcfile
// cant set -l or -i with --rcfile
bashPath := fmt.Sprintf("~/.waveterm/%s/.bashrc", shellutil.BashIntegrationDir)
shellOpts = append(shellOpts, "--rcfile", bashPath)
} else if shellType == shellutil.ShellType_fish {
if cmdOpts.Login {
shellOpts = append(shellOpts, "-l")
}
// source the wave.fish file
waveFishPath := fmt.Sprintf("~/.waveterm/%s/wave.fish", shellutil.FishIntegrationDir)
carg := fmt.Sprintf(`"source %s"`, waveFishPath)
shellOpts = append(shellOpts, "-C", carg)
} else if shellType == shellutil.ShellType_pwsh {
pwshPath := fmt.Sprintf("~/.waveterm/%s/wavepwsh.ps1", shellutil.PwshIntegrationDir)
// powershell is weird about quoted path executables and requires an ampersand first
shellPath = "& " + shellPath
shellOpts = append(shellOpts, "-ExecutionPolicy", "Bypass", "-NoExit", "-File", pwshPath)
} else {
if cmdOpts.Login {
shellOpts = append(shellOpts, "-l")
}
if cmdOpts.Interactive {
shellOpts = append(shellOpts, "-i")
}
// zdotdir setting moved to after session is created
}
cmdCombined = fmt.Sprintf("%s %s", shellPath, strings.Join(shellOpts, " "))
} else {
// TODO check quoting of cmdStr
shellPath = cmdStr
shellOpts = append(shellOpts, "-c", cmdStr)
cmdCombined = fmt.Sprintf("%s %s", shellPath, strings.Join(shellOpts, " "))
}
conn.Infof(logCtx, "starting shell, using command: %s\n", cmdCombined)
conn.Infof(logCtx, "SSH-NEWSESSION (StartRemoteShellProc)\n")
session, err := client.NewSession()
if err != nil {
return nil, err
}
remoteStdinRead, remoteStdinWriteOurs, err := os.Pipe()
if err != nil {
return nil, err
}
remoteStdoutReadOurs, remoteStdoutWrite, err := os.Pipe()
if err != nil {
return nil, err
}
pipePty := &PipePty{
remoteStdinWrite: remoteStdinWriteOurs,
remoteStdoutRead: remoteStdoutReadOurs,
}
if termSize.Rows == 0 || termSize.Cols == 0 {
termSize.Rows = shellutil.DefaultTermRows
termSize.Cols = shellutil.DefaultTermCols
}
if termSize.Rows <= 0 || termSize.Cols <= 0 {
return nil, fmt.Errorf("invalid term size: %v", termSize)
}
session.Stdin = remoteStdinRead
session.Stdout = remoteStdoutWrite
session.Stderr = remoteStdoutWrite
if shellType == shellutil.ShellType_zsh {
zshDir := fmt.Sprintf("~/.waveterm/%s", shellutil.ZshIntegrationDir)
conn.Infof(logCtx, "setting ZDOTDIR to %s\n", zshDir)
cmdCombined = fmt.Sprintf(`ZDOTDIR=%s %s`, zshDir, cmdCombined)
}
packedToken, err := cmdOpts.SwapToken.PackForClient()
if err != nil {
conn.Infof(logCtx, "error packing swap token: %v", err)
} else {
conn.Debugf(logCtx, "packed swaptoken %s\n", packedToken)
cmdCombined = fmt.Sprintf(`%s=%s %s`, wavebase.WaveSwapTokenVarName, packedToken, cmdCombined)
}
shellutil.AddTokenSwapEntry(cmdOpts.SwapToken)
session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil)
sessionWrap := MakeSessionWrap(session, cmdCombined, pipePty)
err = sessionWrap.Start()
if err != nil {
pipePty.Close()
return nil, err
}
return &ShellProc{Cmd: sessionWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
}
func isZshShell(shellPath string) bool {
// get the base path, and then check contains
shellBase := filepath.Base(shellPath)
return strings.Contains(shellBase, "zsh")
}
func isBashShell(shellPath string) bool {
// get the base path, and then check contains
shellBase := filepath.Base(shellPath)
return strings.Contains(shellBase, "bash")
}
func isFishShell(shellPath string) bool {
// get the base path, and then check contains
shellBase := filepath.Base(shellPath)
return strings.Contains(shellBase, "fish")
}
func StartLocalShellProc(logCtx context.Context, termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType) (*ShellProc, error) {
shellutil.InitCustomShellStartupFiles()
var ecmd *exec.Cmd
var shellOpts []string
shellPath := cmdOpts.ShellPath
if shellPath == "" {
shellPath = shellutil.DetectLocalShellPath()
}
shellType := shellutil.GetShellTypeFromShellPath(shellPath)
shellOpts = append(shellOpts, cmdOpts.ShellOpts...)
if cmdStr == "" {
if shellType == shellutil.ShellType_bash {
// add --rcfile
// cant set -l or -i with --rcfile
shellOpts = append(shellOpts, "--rcfile", shellutil.GetLocalBashRcFileOverride())
} else if shellType == shellutil.ShellType_fish {
if cmdOpts.Login {
shellOpts = append(shellOpts, "-l")
}
waveFishPath := shellutil.GetLocalWaveFishFilePath()
carg := fmt.Sprintf("source %s", shellutil.HardQuoteFish(waveFishPath))
shellOpts = append(shellOpts, "-C", carg)
} else if shellType == shellutil.ShellType_pwsh {
shellOpts = append(shellOpts, "-ExecutionPolicy", "Bypass", "-NoExit", "-File", shellutil.GetLocalWavePowershellEnv())
} else {
if cmdOpts.Login {
shellOpts = append(shellOpts, "-l")
}
if cmdOpts.Interactive {
shellOpts = append(shellOpts, "-i")
}
}
blocklogger.Debugf(logCtx, "[conndebug] shell:%s shellOpts:%v\n", shellPath, shellOpts)
ecmd = exec.Command(shellPath, shellOpts...)
ecmd.Env = os.Environ()
if shellType == shellutil.ShellType_zsh {
shellutil.UpdateCmdEnv(ecmd, map[string]string{"ZDOTDIR": shellutil.GetLocalZshZDotDir()})
}
} else {
shellOpts = append(shellOpts, "-c", cmdStr)
ecmd = exec.Command(shellPath, shellOpts...)
ecmd.Env = os.Environ()
}
packedToken, err := cmdOpts.SwapToken.PackForClient()
if err != nil {
blocklogger.Infof(logCtx, "error packing swap token: %v", err)
} else {
blocklogger.Debugf(logCtx, "packed swaptoken %s\n", packedToken)
shellutil.UpdateCmdEnv(ecmd, map[string]string{wavebase.WaveSwapTokenVarName: packedToken})
}
/*
For Snap installations, we need to correct the XDG environment variables as Snap
overrides them to point to snap directories. We will get the correct values, if
set, from the PAM environment. If the XDG variables are set in profile or in an
RC file, it will be overridden when the shell initializes.
*/
if os.Getenv("SNAP") != "" {
log.Printf("Detected Snap installation, correcting XDG environment variables")
varsToReplace := map[string]string{"XDG_CONFIG_HOME": "", "XDG_DATA_HOME": "", "XDG_CACHE_HOME": "", "XDG_RUNTIME_DIR": "", "XDG_CONFIG_DIRS": "", "XDG_DATA_DIRS": ""}
pamEnvs := tryGetPamEnvVars()
if len(pamEnvs) > 0 {
// We only want to set the XDG variables from the PAM environment, all others should already be correct or may have been overridden by something else out of our control
for k := range pamEnvs {
if _, ok := varsToReplace[k]; ok {
varsToReplace[k] = pamEnvs[k]
}
}
}
log.Printf("Setting XDG environment variables to: %v", varsToReplace)
shellutil.UpdateCmdEnv(ecmd, varsToReplace)
}
if cmdOpts.Cwd != "" {
ecmd.Dir = cmdOpts.Cwd
}
if cwdErr := checkCwd(ecmd.Dir); cwdErr != nil {
ecmd.Dir = wavebase.GetHomeDir()
}
envToAdd := shellutil.WaveshellLocalEnvVars(shellutil.DefaultTermType)
if os.Getenv("LANG") == "" {
envToAdd["LANG"] = wavebase.DetermineLang()
}
shellutil.UpdateCmdEnv(ecmd, envToAdd)
if termSize.Rows == 0 || termSize.Cols == 0 {
termSize.Rows = shellutil.DefaultTermRows
termSize.Cols = shellutil.DefaultTermCols
}
if termSize.Rows <= 0 || termSize.Cols <= 0 {
return nil, fmt.Errorf("invalid term size: %v", termSize)
}
shellutil.AddTokenSwapEntry(cmdOpts.SwapToken)
cmdPty, err := pty.StartWithSize(ecmd, &pty.Winsize{Rows: uint16(termSize.Rows), Cols: uint16(termSize.Cols)})
if err != nil {
return nil, err
}
cmdWrap := MakeCmdWrap(ecmd, cmdPty)
return &ShellProc{Cmd: cmdWrap, CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
}
func RunSimpleCmdInPty(ecmd *exec.Cmd, termSize waveobj.TermSize) ([]byte, error) {
ecmd.Env = os.Environ()
shellutil.UpdateCmdEnv(ecmd, shellutil.WaveshellLocalEnvVars(shellutil.DefaultTermType))
if termSize.Rows == 0 || termSize.Cols == 0 {
termSize.Rows = shellutil.DefaultTermRows
termSize.Cols = shellutil.DefaultTermCols
}
if termSize.Rows <= 0 || termSize.Cols <= 0 {
return nil, fmt.Errorf("invalid term size: %v", termSize)
}
cmdPty, err := pty.StartWithSize(ecmd, &pty.Winsize{Rows: uint16(termSize.Rows), Cols: uint16(termSize.Cols)})
if err != nil {
cmdPty.Close()
return nil, err
}
if runtime.GOOS != "windows" {
defer cmdPty.Close()
}
ioDone := make(chan bool)
var outputBuf bytes.Buffer
go func() {
panichandler.PanicHandler("RunSimpleCmdInPty:ioCopy", recover())
// ignore error (/dev/ptmx has read error when process is done)
defer close(ioDone)
io.Copy(&outputBuf, cmdPty)
}()
exitErr := ecmd.Wait()
if exitErr != nil {
return nil, exitErr
}
<-ioDone
return outputBuf.Bytes(), nil
}
const etcEnvironmentPath = "/etc/environment"
const etcSecurityPath = "/etc/security/pam_env.conf"
const userEnvironmentPath = "~/.pam_environment"
var pamParseOpts *pamparse.PamParseOpts = pamparse.ParsePasswdSafe()
/*
tryGetPamEnvVars tries to get the environment variables from /etc/environment,
/etc/security/pam_env.conf, and ~/.pam_environment.
It then returns a map of the environment variables, overriding duplicates with
the following order of precedence:
1. /etc/environment
2. /etc/security/pam_env.conf
3. ~/.pam_environment
*/
func tryGetPamEnvVars() map[string]string {
envVars, err := pamparse.ParseEnvironmentFile(etcEnvironmentPath)
if err != nil {
log.Printf("error parsing %s: %v", etcEnvironmentPath, err)
}
envVars2, err := pamparse.ParseEnvironmentConfFile(etcSecurityPath, pamParseOpts)
if err != nil {
log.Printf("error parsing %s: %v", etcSecurityPath, err)
}
envVars3, err := pamparse.ParseEnvironmentConfFile(wavebase.ExpandHomeDirSafe(userEnvironmentPath), pamParseOpts)
if err != nil {
log.Printf("error parsing %s: %v", userEnvironmentPath, err)
}
for k, v := range envVars2 {
envVars[k] = v
}
for k, v := range envVars3 {
envVars[k] = v
}
return envVars
}