2024-05-15 01:53:03 +02:00
|
|
|
// Copyright 2024, Command Line Inc.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
package shellexec
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
2024-06-12 20:33:45 +02:00
|
|
|
"reflect"
|
2024-05-15 01:53:03 +02:00
|
|
|
"syscall"
|
|
|
|
|
|
|
|
"github.com/creack/pty"
|
|
|
|
"github.com/wavetermdev/thenextwave/pkg/util/shellutil"
|
2024-05-16 09:29:58 +02:00
|
|
|
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
2024-05-15 01:53:03 +02:00
|
|
|
)
|
|
|
|
|
2024-05-15 07:37:04 +02:00
|
|
|
type TermSize struct {
|
|
|
|
Rows int `json:"rows"`
|
|
|
|
Cols int `json:"cols"`
|
|
|
|
}
|
|
|
|
|
2024-05-15 08:25:21 +02:00
|
|
|
type ShellProc struct {
|
|
|
|
Cmd *exec.Cmd
|
|
|
|
Pty *os.File
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sp *ShellProc) Close() {
|
|
|
|
sp.Cmd.Process.Kill()
|
|
|
|
go func() {
|
|
|
|
sp.Cmd.Process.Wait()
|
|
|
|
sp.Pty.Close()
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2024-06-12 20:33:45 +02:00
|
|
|
func setBoolConditionally(rval reflect.Value, field string, value bool) {
|
|
|
|
if rval.Elem().FieldByName(field).IsValid() {
|
|
|
|
rval.Elem().FieldByName(field).SetBool(value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func setSysProcAttrs(cmd *exec.Cmd) {
|
|
|
|
rval := reflect.ValueOf(cmd.SysProcAttr)
|
|
|
|
setBoolConditionally(rval, "Setsid", true)
|
|
|
|
setBoolConditionally(rval, "Setctty", true)
|
|
|
|
}
|
|
|
|
|
2024-05-15 08:25:21 +02:00
|
|
|
func StartShellProc(termSize TermSize) (*ShellProc, error) {
|
|
|
|
shellPath := shellutil.DetectLocalShellPath()
|
|
|
|
ecmd := exec.Command(shellPath, "-i", "-l")
|
|
|
|
ecmd.Env = os.Environ()
|
2024-06-12 02:42:10 +02:00
|
|
|
ecmd.Dir = wavebase.GetHomeDir()
|
2024-05-16 09:29:58 +02:00
|
|
|
envToAdd := shellutil.WaveshellEnvVars(shellutil.DefaultTermType)
|
|
|
|
if os.Getenv("LANG") == "" {
|
|
|
|
envToAdd["LANG"] = wavebase.DetermineLang()
|
|
|
|
}
|
|
|
|
shellutil.UpdateCmdEnv(ecmd, envToAdd)
|
2024-05-15 08:25:21 +02:00
|
|
|
cmdPty, cmdTty, err := pty.Open()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("opening new pty: %w", err)
|
|
|
|
}
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
pty.Setsize(cmdPty, &pty.Winsize{Rows: uint16(termSize.Rows), Cols: uint16(termSize.Cols)})
|
|
|
|
ecmd.Stdin = cmdTty
|
|
|
|
ecmd.Stdout = cmdTty
|
|
|
|
ecmd.Stderr = cmdTty
|
|
|
|
ecmd.SysProcAttr = &syscall.SysProcAttr{}
|
2024-06-12 20:33:45 +02:00
|
|
|
setSysProcAttrs(ecmd)
|
2024-05-15 08:25:21 +02:00
|
|
|
err = ecmd.Start()
|
|
|
|
cmdTty.Close()
|
|
|
|
if err != nil {
|
|
|
|
cmdPty.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &ShellProc{Cmd: ecmd, Pty: cmdPty}, nil
|
|
|
|
}
|
|
|
|
|
2024-05-15 07:37:04 +02:00
|
|
|
func RunSimpleCmdInPty(ecmd *exec.Cmd, termSize TermSize) ([]byte, error) {
|
2024-05-15 01:53:03 +02:00
|
|
|
ecmd.Env = os.Environ()
|
|
|
|
shellutil.UpdateCmdEnv(ecmd, shellutil.WaveshellEnvVars(shellutil.DefaultTermType))
|
|
|
|
cmdPty, cmdTty, err := pty.Open()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("opening new pty: %w", err)
|
|
|
|
}
|
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)
|
|
|
|
}
|
|
|
|
pty.Setsize(cmdPty, &pty.Winsize{Rows: uint16(termSize.Rows), Cols: uint16(termSize.Cols)})
|
2024-05-15 01:53:03 +02:00
|
|
|
ecmd.Stdin = cmdTty
|
|
|
|
ecmd.Stdout = cmdTty
|
|
|
|
ecmd.Stderr = cmdTty
|
|
|
|
ecmd.SysProcAttr = &syscall.SysProcAttr{}
|
2024-06-12 20:33:45 +02:00
|
|
|
setSysProcAttrs(ecmd)
|
2024-05-15 01:53:03 +02:00
|
|
|
err = ecmd.Start()
|
|
|
|
cmdTty.Close()
|
|
|
|
if err != nil {
|
|
|
|
cmdPty.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer cmdPty.Close()
|
|
|
|
ioDone := make(chan bool)
|
|
|
|
var outputBuf bytes.Buffer
|
|
|
|
go func() {
|
|
|
|
// 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
|
|
|
|
}
|