// Copyright 2023, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "os" "strings" "syscall" "time" "github.com/wavetermdev/waveterm/waveshell/pkg/base" "github.com/wavetermdev/waveterm/waveshell/pkg/packet" "github.com/wavetermdev/waveterm/waveshell/pkg/server" "github.com/wavetermdev/waveterm/waveshell/pkg/shexec" "github.com/wavetermdev/waveterm/waveshell/pkg/wlog" ) var BuildTime = "0" func readFullRunPacket(packetParser *packet.PacketParser) (*packet.RunPacketType, error) { rpb := packet.MakeRunPacketBuilder() for pk := range packetParser.MainCh { ok, runPacket := rpb.ProcessPacket(pk) if runPacket != nil { return runPacket, nil } if !ok { return nil, fmt.Errorf("invalid packet '%s' sent to mshell", pk.GetType()) } } return nil, fmt.Errorf("no run packet received") } func handleSingle() { packetParser := packet.MakePacketParser(os.Stdin, nil) sender := packet.MakePacketSender(os.Stdout, nil) defer func() { sender.Close() sender.WaitForDone() }() wlog.LogConsumer = sender.SendLogPacket initPacket := shexec.MakeInitPacket() sender.SendPacket(initPacket) if len(os.Args) >= 3 && os.Args[2] == "--version" { return } runPacket, err := readFullRunPacket(packetParser) if err != nil { sender.SendErrorResponse(runPacket.ReqId, err) return } err = shexec.ValidateRunPacket(runPacket) if err != nil { sender.SendErrorResponse(runPacket.ReqId, err) return } err = runPacket.CK.Validate("run packet") if err != nil { sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("run packets from server must have a CK: %v", err)) } if runPacket.Detached { cmd, startPk, err := shexec.RunCommandDetached(runPacket, sender) if err != nil { sender.SendErrorResponse(runPacket.ReqId, err) return } sender.SendPacket(startPk) sender.Close() sender.WaitForDone() cmd.DetachedWait(startPk) return } else { shexec.IgnoreSigPipe() ticker := time.NewTicker(1 * time.Minute) go func() { for range ticker.C { // this will let the command detect when the server has gone away // that will then trigger cmd.SendHup() to send SIGHUP to the exec'ed process sender.SendPacket(packet.MakePingPacket()) } }() defer ticker.Stop() cmd, err := shexec.RunCommandSimple(runPacket, sender, true) if err != nil { sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("error running command: %w", err)) return } defer cmd.Close() startPacket := cmd.MakeCmdStartPacket(runPacket.ReqId) sender.SendPacket(startPacket) go func() { exitErr := sender.WaitForDone() if exitErr != nil { base.Logf("I/O error talking to server, sending SIGHUP to children\n") cmd.SendSignal(syscall.SIGHUP) } }() cmd.RunRemoteIOAndWait(packetParser, sender) return } } func handleUsage() { usage := ` mshell is a helper program for wave terminal. it is used to execute commands Options: --help - prints this message --version - print version --server - multiplexer to run multiple commands --single - run a single command (connected to multiplexer) --single --version - return an init packet with version info mshell does not open any external ports and does not require any additional permissions. it communicates exclusively through stdin/stdout with an attached process via a JSON packet format. ` fmt.Printf("%s\n\n", strings.TrimSpace(usage)) } func main() { base.SetBuildTime(BuildTime) if len(os.Args) == 1 { handleUsage() return } firstArg := os.Args[1] if firstArg == "--help" { handleUsage() return } else if firstArg == "--version" { fmt.Printf("mshell %s+%s\n", base.MShellVersion, base.BuildTime) return } else if firstArg == "--single" || firstArg == "--single-from-server" { base.ProcessType = base.ProcessType_WaveShellSingle wlog.GlobalSubsystem = base.ProcessType_WaveShellSingle base.InitDebugLog("single") handleSingle() return } else if firstArg == "--server" { base.ProcessType = base.ProcessType_WaveShellServer wlog.GlobalSubsystem = base.ProcessType_WaveShellServer base.InitDebugLog("server") rtnCode, err := server.RunServer() if err != nil { fmt.Fprintf(os.Stderr, "[error] %v\n", err) } if rtnCode != 0 { os.Exit(rtnCode) } return } else { handleUsage() return } }