2025-01-05 05:56:57 +01:00
// Copyright 2025, Command Line Inc.
2024-05-15 01:53:03 +02:00
// SPDX-License-Identifier: Apache-2.0
package shellexec
import (
"bytes"
2024-10-24 07:43:17 +02:00
"context"
2024-05-15 01:53:03 +02:00
"fmt"
"io"
2024-07-16 03:00:10 +02:00
"log"
2024-05-15 01:53:03 +02:00
"os"
"os/exec"
2024-08-01 08:47:33 +02:00
"path/filepath"
2024-08-10 03:49:35 +02:00
"runtime"
2024-07-16 03:00:10 +02:00
"strings"
2024-06-24 23:34:31 +02:00
"sync"
2024-05-15 01:53:03 +02:00
"syscall"
2024-09-05 09:21:08 +02:00
"time"
2024-05-15 01:53:03 +02:00
"github.com/creack/pty"
2025-01-16 20:17:29 +01:00
"github.com/wavetermdev/waveterm/pkg/blocklogger"
2024-11-21 03:05:13 +01:00
"github.com/wavetermdev/waveterm/pkg/panichandler"
2024-09-05 23:25:45 +02:00
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
2025-01-10 20:06:15 +01:00
"github.com/wavetermdev/waveterm/pkg/util/pamparse"
2024-09-05 23:25:45 +02:00
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/waveobj"
2025-01-13 00:22:07 +01:00
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
2024-09-05 23:25:45 +02:00
"github.com/wavetermdev/waveterm/pkg/wshutil"
2025-01-17 00:54:58 +01:00
"github.com/wavetermdev/waveterm/pkg/wslconn"
2024-05-15 01:53:03 +02:00
)
2024-09-05 09:21:08 +02:00
const DefaultGracefulKillWait = 400 * time . Millisecond
2024-06-24 23:34:31 +02:00
type CommandOptsType struct {
2025-01-16 20:17:29 +01:00
Interactive bool ` json:"interactive,omitempty" `
Login bool ` json:"login,omitempty" `
Cwd string ` json:"cwd,omitempty" `
Env map [ string ] string ` json:"env,omitempty" `
ShellPath string ` json:"shellPath,omitempty" `
ShellOpts [ ] string ` json:"shellOpts,omitempty" `
SwapToken * shellutil . TokenSwapEntry ` json:"swapToken,omitempty" `
2024-06-24 23:34:31 +02:00
}
2024-05-15 08:25:21 +02:00
type ShellProc struct {
2024-09-05 09:21:08 +02:00
ConnName string
2024-07-16 03:00:10 +02:00
Cmd ConnInterface
2024-06-24 23:34:31 +02:00
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
2024-05-15 08:25:21 +02:00
}
func ( sp * ShellProc ) Close ( ) {
2024-09-05 09:21:08 +02:00
sp . Cmd . KillGraceful ( DefaultGracefulKillWait )
2024-05-15 08:25:21 +02:00
go func ( ) {
2024-12-31 18:31:55 +01:00
defer func ( ) {
panichandler . PanicHandler ( "ShellProc.Close" , recover ( ) )
} ( )
2024-07-16 03:00:10 +02:00
waitErr := sp . Cmd . Wait ( )
2024-06-24 23:34:31 +02:00
sp . SetWaitErrorAndSignalDone ( waitErr )
2024-08-10 03:49:35 +02:00
// windows cannot handle the pty being
// closed twice, so we let the pty
// close itself instead
if runtime . GOOS != "windows" {
sp . Cmd . Close ( )
}
2024-05-15 08:25:21 +02:00
} ( )
}
2024-06-24 23:34:31 +02:00
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
}
2024-08-10 03:49:35 +02:00
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 ) )
}
2025-01-17 00:54:58 +01:00
func StartWslShellProc ( ctx context . Context , termSize waveobj . TermSize , cmdStr string , cmdOpts CommandOptsType , conn * wslconn . WslConn ) ( * ShellProc , error ) {
2024-10-24 07:43:17 +02:00
client := conn . GetClient ( )
2025-01-17 00:54:58 +01:00
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
}
2024-10-24 07:43:17 +02:00
if shellPath == "" {
2025-01-17 00:54:58 +01:00
conn . Infof ( ctx , "no shell path detected, using default (/bin/bash)\n" )
shellPath = "/bin/bash"
2024-10-24 07:43:17 +02:00
}
var shellOpts [ ] string
2025-01-17 00:54:58 +01:00
var cmdCombined string
log . Printf ( "detected shell %q for conn %q\n" , shellPath , conn . GetName ( ) )
2024-10-24 07:43:17 +02:00
2025-01-17 00:54:58 +01:00
err = wshclient . RemoteInstallRcFilesCommand ( rpcClient , & wshrpc . RpcOpts { Route : connRoute , Timeout : 2000 } )
2024-10-24 07:43:17 +02:00
if err != nil {
log . Printf ( "error installing rc files: %v" , err )
return nil , err
}
2025-01-17 00:54:58 +01:00
shellOpts = append ( shellOpts , cmdOpts . ShellOpts ... )
shellType := shellutil . GetShellTypeFromShellPath ( shellPath )
conn . Infof ( ctx , "detected shell type: %s\n" , shellType )
2024-10-24 07:43:17 +02:00
if cmdStr == "" {
/* transform command in order to inject environment vars */
2025-01-17 00:54:58 +01:00
if shellType == shellutil . ShellType_bash {
2024-10-24 07:43:17 +02:00
// add --rcfile
// cant set -l or -i with --rcfile
2025-01-17 00:54:58 +01:00
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 )
2024-10-24 07:43:17 +02:00
// powershell is weird about quoted path executables and requires an ampersand first
shellPath = "& " + shellPath
2025-01-17 00:54:58 +01:00
shellOpts = append ( shellOpts , "-ExecutionPolicy" , "Bypass" , "-NoExit" , "-File" , pwshPath )
2024-10-24 07:43:17 +02:00
} else {
if cmdOpts . Login {
2025-01-17 00:54:58 +01:00
shellOpts = append ( shellOpts , "-l" )
2024-10-24 07:43:17 +02:00
}
if cmdOpts . Interactive {
2025-01-17 00:54:58 +01:00
shellOpts = append ( shellOpts , "-i" )
2024-10-24 07:43:17 +02:00
}
2025-01-17 00:54:58 +01:00
// zdotdir setting moved to after session is created
2024-10-24 07:43:17 +02:00
}
2025-01-17 00:54:58 +01:00
cmdCombined = fmt . Sprintf ( "%s %s" , shellPath , strings . Join ( shellOpts , " " ) )
2024-10-24 07:43:17 +02:00
} else {
2025-01-17 00:54:58 +01:00
// TODO check quoting of cmdStr
2024-10-24 07:43:17 +02:00
shellPath = cmdStr
2025-01-17 00:54:58 +01:00
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 )
2024-10-24 07:43:17 +02:00
}
jwtToken , ok := cmdOpts . Env [ wshutil . WaveJwtTokenVarName ]
if ! ok {
return nil , fmt . Errorf ( "no jwt token provided to connection" )
}
2025-01-17 00:54:58 +01:00
cmdCombined = fmt . Sprintf ( ` %s=%s %s ` , wshutil . WaveJwtTokenVarName , jwtToken , cmdCombined )
2024-10-24 07:43:17 +02:00
2025-01-17 00:54:58 +01:00
log . Printf ( "full combined command: %s" , cmdCombined )
ecmd := exec . Command ( "wsl.exe" , "~" , "-d" , client . Name ( ) , "--" , "sh" , "-c" , cmdCombined )
2024-10-24 07:43:17 +02:00
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 )
}
2025-01-17 00:54:58 +01:00
shellutil . AddTokenSwapEntry ( cmdOpts . SwapToken )
2024-10-24 07:43:17 +02:00
cmdPty , err := pty . StartWithSize ( ecmd , & pty . Winsize { Rows : uint16 ( termSize . Rows ) , Cols : uint16 ( termSize . Cols ) } )
if err != nil {
return nil , err
}
2024-12-04 23:16:50 +01:00
cmdWrap := MakeCmdWrap ( ecmd , cmdPty )
return & ShellProc { Cmd : cmdWrap , ConnName : conn . GetName ( ) , CloseOnce : & sync . Once { } , DoneCh : make ( chan any ) } , nil
2024-10-24 07:43:17 +02:00
}
2025-01-14 23:09:26 +01:00
func StartRemoteShellProcNoWsh ( ctx context . Context , termSize waveobj . TermSize , cmdStr string , cmdOpts CommandOptsType , conn * conncontroller . SSHConn ) ( * ShellProc , error ) {
2024-09-05 09:21:08 +02:00
client := conn . GetClient ( )
2025-01-14 23:09:26 +01:00
conn . Infof ( ctx , "SSH-NEWSESSION (StartRemoteShellProcNoWsh)" )
2024-12-06 19:11:38 +01:00
session , err := client . NewSession ( )
if err != nil {
return nil , err
}
2024-11-28 01:52:00 +01:00
2024-12-06 19:11:38 +01:00
remoteStdinRead , remoteStdinWriteOurs , err := os . Pipe ( )
if err != nil {
return nil , err
}
2024-11-28 01:52:00 +01:00
2024-12-06 19:11:38 +01:00
remoteStdoutReadOurs , remoteStdoutWrite , err := os . Pipe ( )
if err != nil {
return nil , err
}
2024-11-28 01:52:00 +01:00
2024-12-06 19:11:38 +01:00
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
2024-11-28 01:52:00 +01:00
2024-12-06 19:11:38 +01:00
session . RequestPty ( "xterm-256color" , termSize . Rows , termSize . Cols , nil )
sessionWrap := MakeSessionWrap ( session , "" , pipePty )
err = session . Shell ( )
if err != nil {
pipePty . Close ( )
return nil , err
2024-11-28 01:52:00 +01:00
}
2024-12-06 19:11:38 +01:00
return & ShellProc { Cmd : sessionWrap , ConnName : conn . GetName ( ) , CloseOnce : & sync . Once { } , DoneCh : make ( chan any ) } , nil
}
2025-01-16 20:17:29 +01:00
func StartRemoteShellProc ( ctx context . Context , logCtx context . Context , termSize waveobj . TermSize , cmdStr string , cmdOpts CommandOptsType , conn * conncontroller . SSHConn ) ( * ShellProc , error ) {
2024-12-06 19:11:38 +01:00
client := conn . GetClient ( )
2025-01-13 00:22:07 +01:00
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 )
2025-01-14 23:09:26 +01:00
var shellPath string
if cmdOpts . ShellPath != "" {
2025-01-16 20:17:29 +01:00
conn . Infof ( logCtx , "using shell path from command opts: %s\n" , cmdOpts . ShellPath )
2025-01-14 23:09:26 +01:00
shellPath = cmdOpts . ShellPath
}
configShellPath := conn . GetConfigShellPath ( )
if shellPath == "" && configShellPath != "" {
2025-01-16 20:17:29 +01:00
conn . Infof ( logCtx , "using shell path from config (conn:shellpath): %s\n" , configShellPath )
2025-01-14 23:09:26 +01:00
shellPath = configShellPath
}
if shellPath == "" && remoteInfo . Shell != "" {
2025-01-16 20:17:29 +01:00
conn . Infof ( logCtx , "using shell path detected on remote machine: %s\n" , remoteInfo . Shell )
2025-01-13 00:22:07 +01:00
shellPath = remoteInfo . Shell
2024-08-16 06:32:08 +02:00
}
2025-01-14 23:09:26 +01:00
if shellPath == "" {
2025-01-16 20:17:29 +01:00
conn . Infof ( logCtx , "no shell path detected, using default (/bin/bash)\n" )
2025-01-14 23:09:26 +01:00
shellPath = "/bin/bash"
}
2024-08-16 06:32:08 +02:00
var shellOpts [ ] string
var cmdCombined string
2025-01-14 23:09:26 +01:00
log . Printf ( "detected shell %q for conn %q\n" , shellPath , conn . GetName ( ) )
2024-10-01 06:19:07 +02:00
shellOpts = append ( shellOpts , cmdOpts . ShellOpts ... )
2025-01-15 00:29:36 +01:00
shellType := shellutil . GetShellTypeFromShellPath ( shellPath )
2025-01-16 20:17:29 +01:00
conn . Infof ( logCtx , "detected shell type: %s\n" , shellType )
conn . Infof ( logCtx , "swaptoken: %s\n" , cmdOpts . SwapToken . Token )
2024-08-16 06:32:08 +02:00
2024-07-16 03:00:10 +02:00
if cmdStr == "" {
2024-08-16 06:32:08 +02:00
/* transform command in order to inject environment vars */
2025-01-15 00:29:36 +01:00
if shellType == shellutil . ShellType_bash {
2024-08-16 06:32:08 +02:00
// add --rcfile
// cant set -l or -i with --rcfile
2025-01-14 23:09:26 +01:00
bashPath := fmt . Sprintf ( "~/.waveterm/%s/.bashrc" , shellutil . BashIntegrationDir )
2025-01-13 00:22:07 +01:00
shellOpts = append ( shellOpts , "--rcfile" , bashPath )
2025-01-15 00:29:36 +01:00
} else if shellType == shellutil . ShellType_fish {
2025-01-14 23:09:26 +01:00
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 )
2024-09-27 00:34:52 +02:00
shellOpts = append ( shellOpts , "-C" , carg )
2025-01-15 00:29:36 +01:00
} else if shellType == shellutil . ShellType_pwsh {
2025-01-14 23:09:26 +01:00
pwshPath := fmt . Sprintf ( "~/.waveterm/%s/wavepwsh.ps1" , shellutil . PwshIntegrationDir )
2024-09-04 11:13:00 +02:00
// powershell is weird about quoted path executables and requires an ampersand first
shellPath = "& " + shellPath
2025-01-13 00:22:07 +01:00
shellOpts = append ( shellOpts , "-ExecutionPolicy" , "Bypass" , "-NoExit" , "-File" , pwshPath )
2024-08-16 06:32:08 +02:00
} else {
if cmdOpts . Login {
shellOpts = append ( shellOpts , "-l" )
2025-01-14 23:09:26 +01:00
}
if cmdOpts . Interactive {
2024-08-16 06:32:08 +02:00
shellOpts = append ( shellOpts , "-i" )
}
// zdotdir setting moved to after session is created
}
cmdCombined = fmt . Sprintf ( "%s %s" , shellPath , strings . Join ( shellOpts , " " ) )
2024-07-16 03:00:10 +02:00
} else {
2025-01-14 23:09:26 +01:00
// TODO check quoting of cmdStr
2024-07-16 03:00:10 +02:00
shellPath = cmdStr
2024-08-16 06:32:08 +02:00
shellOpts = append ( shellOpts , "-c" , cmdStr )
cmdCombined = fmt . Sprintf ( "%s %s" , shellPath , strings . Join ( shellOpts , " " ) )
2024-07-16 03:00:10 +02:00
}
2025-01-16 20:17:29 +01:00
conn . Infof ( logCtx , "starting shell, using command: %s\n" , cmdCombined )
conn . Infof ( logCtx , "SSH-NEWSESSION (StartRemoteShellProc)\n" )
2024-07-16 03:00:10 +02:00
session , err := client . NewSession ( )
if err != nil {
return nil , err
}
2024-08-10 03:49:35 +02:00
remoteStdinRead , remoteStdinWriteOurs , err := os . Pipe ( )
if err != nil {
return nil , err
}
remoteStdoutReadOurs , remoteStdoutWrite , err := os . Pipe ( )
2024-07-16 03:00:10 +02:00
if err != nil {
2024-08-10 03:49:35 +02:00
return nil , err
}
pipePty := & PipePty {
remoteStdinWrite : remoteStdinWriteOurs ,
remoteStdoutRead : remoteStdoutReadOurs ,
2024-07-16 03:00:10 +02:00
}
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 )
}
2024-08-10 03:49:35 +02:00
session . Stdin = remoteStdinRead
session . Stdout = remoteStdoutWrite
session . Stderr = remoteStdoutWrite
2024-08-16 06:32:08 +02:00
2024-07-23 22:16:53 +02:00
for envKey , envVal := range cmdOpts . Env {
// note these might fail depending on server settings, but we still try
session . Setenv ( envKey , envVal )
}
2025-01-15 00:29:36 +01:00
if shellType == shellutil . ShellType_zsh {
2025-01-14 23:09:26 +01:00
zshDir := fmt . Sprintf ( "~/.waveterm/%s" , shellutil . ZshIntegrationDir )
2025-01-16 20:17:29 +01:00
conn . Infof ( logCtx , "setting ZDOTDIR to %s\n" , zshDir )
2025-01-13 00:22:07 +01:00
cmdCombined = fmt . Sprintf ( ` ZDOTDIR=%s %s ` , zshDir , cmdCombined )
2024-08-16 06:32:08 +02:00
}
2025-01-16 20:17:29 +01:00
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 )
2024-08-17 20:21:25 +02:00
}
2025-01-16 20:17:29 +01:00
shellutil . AddTokenSwapEntry ( cmdOpts . SwapToken )
2024-07-16 03:00:10 +02:00
session . RequestPty ( "xterm-256color" , termSize . Rows , termSize . Cols , nil )
2024-12-04 23:16:50 +01:00
sessionWrap := MakeSessionWrap ( session , cmdCombined , pipePty )
2024-07-16 03:00:10 +02:00
err = sessionWrap . Start ( )
if err != nil {
2024-08-10 03:49:35 +02:00
pipePty . Close ( )
2024-07-16 03:00:10 +02:00
return nil , err
}
2024-09-05 09:21:08 +02:00
return & ShellProc { Cmd : sessionWrap , ConnName : conn . GetName ( ) , CloseOnce : & sync . Once { } , DoneCh : make ( chan any ) } , nil
2024-07-16 03:00:10 +02:00
}
2024-08-01 08:47:33 +02:00
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" )
}
2024-09-27 00:34:52 +02:00
func isFishShell ( shellPath string ) bool {
// get the base path, and then check contains
shellBase := filepath . Base ( shellPath )
return strings . Contains ( shellBase , "fish" )
}
2025-01-16 20:17:29 +01:00
func StartLocalShellProc ( logCtx context . Context , termSize waveobj . TermSize , cmdStr string , cmdOpts CommandOptsType ) ( * ShellProc , error ) {
2024-08-01 08:47:33 +02:00
shellutil . InitCustomShellStartupFiles ( )
2024-06-24 23:34:31 +02:00
var ecmd * exec . Cmd
var shellOpts [ ] string
2024-09-27 00:34:52 +02:00
shellPath := cmdOpts . ShellPath
if shellPath == "" {
shellPath = shellutil . DetectLocalShellPath ( )
}
2025-01-15 00:29:36 +01:00
shellType := shellutil . GetShellTypeFromShellPath ( shellPath )
2024-10-01 06:19:07 +02:00
shellOpts = append ( shellOpts , cmdOpts . ShellOpts ... )
2024-06-24 23:34:31 +02:00
if cmdStr == "" {
2025-01-15 00:29:36 +01:00
if shellType == shellutil . ShellType_bash {
2024-08-01 08:47:33 +02:00
// add --rcfile
// cant set -l or -i with --rcfile
2025-01-14 23:09:26 +01:00
shellOpts = append ( shellOpts , "--rcfile" , shellutil . GetLocalBashRcFileOverride ( ) )
2025-01-15 00:29:36 +01:00
} else if shellType == shellutil . ShellType_fish {
2025-01-14 23:09:26 +01:00
if cmdOpts . Login {
shellOpts = append ( shellOpts , "-l" )
}
waveFishPath := shellutil . GetLocalWaveFishFilePath ( )
2025-01-15 00:29:36 +01:00
carg := fmt . Sprintf ( "source %s" , shellutil . HardQuoteFish ( waveFishPath ) )
2025-01-14 23:09:26 +01:00
shellOpts = append ( shellOpts , "-C" , carg )
2025-01-15 00:29:36 +01:00
} else if shellType == shellutil . ShellType_pwsh {
2025-01-14 23:09:26 +01:00
shellOpts = append ( shellOpts , "-ExecutionPolicy" , "Bypass" , "-NoExit" , "-File" , shellutil . GetLocalWavePowershellEnv ( ) )
2024-09-04 11:13:00 +02:00
} else {
2024-08-01 08:47:33 +02:00
if cmdOpts . Login {
shellOpts = append ( shellOpts , "-l" )
2025-01-14 23:09:26 +01:00
}
if cmdOpts . Interactive {
2024-08-01 08:47:33 +02:00
shellOpts = append ( shellOpts , "-i" )
}
}
2025-01-16 20:17:29 +01:00
blocklogger . Debugf ( logCtx , "[conndebug] shell:%s shellOpts:%v\n" , shellPath , shellOpts )
2024-06-24 23:34:31 +02:00
ecmd = exec . Command ( shellPath , shellOpts ... )
2024-08-01 08:47:33 +02:00
ecmd . Env = os . Environ ( )
2025-01-15 00:29:36 +01:00
if shellType == shellutil . ShellType_zsh {
2025-01-14 23:09:26 +01:00
shellutil . UpdateCmdEnv ( ecmd , map [ string ] string { "ZDOTDIR" : shellutil . GetLocalZshZDotDir ( ) } )
2024-08-01 08:47:33 +02:00
}
2024-06-24 23:34:31 +02:00
} else {
shellOpts = append ( shellOpts , "-c" , cmdStr )
ecmd = exec . Command ( shellPath , shellOpts ... )
2024-08-01 08:47:33 +02:00
ecmd . Env = os . Environ ( )
2024-06-24 23:34:31 +02:00
}
2025-01-10 20:06:15 +01:00
2025-01-16 20:17:29 +01:00
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 } )
}
2025-01-10 20:06:15 +01:00
/ *
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" ) != "" {
2025-01-10 20:20:45 +01:00
log . Printf ( "Detected Snap installation, correcting XDG environment variables" )
2025-01-10 20:06:15 +01:00
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 ]
}
}
}
2025-01-10 20:20:45 +01:00
log . Printf ( "Setting XDG environment variables to: %v" , varsToReplace )
2025-01-10 20:06:15 +01:00
shellutil . UpdateCmdEnv ( ecmd , varsToReplace )
}
2024-06-24 23:34:31 +02:00
if cmdOpts . Cwd != "" {
ecmd . Dir = cmdOpts . Cwd
}
if cwdErr := checkCwd ( ecmd . Dir ) ; cwdErr != nil {
ecmd . Dir = wavebase . GetHomeDir ( )
}
2024-08-01 08:47:33 +02:00
envToAdd := shellutil . WaveshellLocalEnvVars ( shellutil . DefaultTermType )
2024-05-16 09:29:58 +02:00
if os . Getenv ( "LANG" ) == "" {
envToAdd [ "LANG" ] = wavebase . DetermineLang ( )
}
shellutil . UpdateCmdEnv ( ecmd , envToAdd )
2024-07-23 22:16:53 +02:00
shellutil . UpdateCmdEnv ( ecmd , cmdOpts . Env )
2024-05-15 08:25:21 +02:00
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 )
}
2025-01-16 20:17:29 +01:00
shellutil . AddTokenSwapEntry ( cmdOpts . SwapToken )
2024-08-10 03:49:35 +02:00
cmdPty , err := pty . StartWithSize ( ecmd , & pty . Winsize { Rows : uint16 ( termSize . Rows ) , Cols : uint16 ( termSize . Cols ) } )
2024-05-15 08:25:21 +02:00
if err != nil {
return nil , err
}
2024-12-04 23:16:50 +01:00
cmdWrap := MakeCmdWrap ( ecmd , cmdPty )
return & ShellProc { Cmd : cmdWrap , CloseOnce : & sync . Once { } , DoneCh : make ( chan any ) } , nil
2024-05-15 08:25:21 +02:00
}
2024-08-20 23:56:48 +02:00
func RunSimpleCmdInPty ( ecmd * exec . Cmd , termSize waveobj . TermSize ) ( [ ] byte , error ) {
2024-05-15 01:53:03 +02:00
ecmd . Env = os . Environ ( )
2024-08-01 08:47:33 +02:00
shellutil . UpdateCmdEnv ( ecmd , shellutil . WaveshellLocalEnvVars ( shellutil . DefaultTermType ) )
2024-05-15 07:37:04 +02:00
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 )
}
2024-08-10 03:49:35 +02:00
cmdPty , err := pty . StartWithSize ( ecmd , & pty . Winsize { Rows : uint16 ( termSize . Rows ) , Cols : uint16 ( termSize . Cols ) } )
2024-05-15 01:53:03 +02:00
if err != nil {
cmdPty . Close ( )
return nil , err
}
2024-08-10 03:49:35 +02:00
if runtime . GOOS != "windows" {
defer cmdPty . Close ( )
}
2024-05-15 01:53:03 +02:00
ioDone := make ( chan bool )
var outputBuf bytes . Buffer
go func ( ) {
2024-12-31 18:31:55 +01:00
panichandler . PanicHandler ( "RunSimpleCmdInPty:ioCopy" , recover ( ) )
2024-05-15 01:53:03 +02:00
// 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
}
2025-01-10 20:06:15 +01:00
const etcEnvironmentPath = "/etc/environment"
const etcSecurityPath = "/etc/security/pam_env.conf"
const userEnvironmentPath = "~/.pam_environment"
2025-01-10 22:53:24 +01:00
var pamParseOpts * pamparse . PamParseOpts = pamparse . ParsePasswdSafe ( )
2025-01-10 20:06:15 +01:00
/ *
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 )
}
2025-01-10 22:53:24 +01:00
envVars2 , err := pamparse . ParseEnvironmentConfFile ( etcSecurityPath , pamParseOpts )
2025-01-10 20:06:15 +01:00
if err != nil {
log . Printf ( "error parsing %s: %v" , etcSecurityPath , err )
}
2025-01-10 22:53:24 +01:00
envVars3 , err := pamparse . ParseEnvironmentConfFile ( wavebase . ExpandHomeDirSafe ( userEnvironmentPath ) , pamParseOpts )
2025-01-10 20:06:15 +01:00
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
}