2017-10-14 11:13:47 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"os/signal"
|
|
|
|
"syscall"
|
2018-05-12 21:44:59 +00:00
|
|
|
"time"
|
2017-10-14 11:13:47 +00:00
|
|
|
|
|
|
|
ptyDevice "github.com/elisescu/pty"
|
2020-09-26 08:41:54 +00:00
|
|
|
log "github.com/sirupsen/logrus"
|
2020-10-03 11:22:25 +00:00
|
|
|
"golang.org/x/crypto/ssh/terminal"
|
2017-10-14 11:13:47 +00:00
|
|
|
)
|
|
|
|
|
2020-09-26 08:41:54 +00:00
|
|
|
type onWindowChangedCB func(int, int)
|
2017-10-14 11:13:47 +00:00
|
|
|
|
|
|
|
// This defines a PTY Master whih will encapsulate the command we want to run, and provide simple
|
|
|
|
// access to the command, to write and read IO, but also to control the window size.
|
|
|
|
type ptyMaster struct {
|
|
|
|
ptyFile *os.File
|
|
|
|
command *exec.Cmd
|
|
|
|
terminalInitState *terminal.State
|
2022-11-18 10:59:56 +00:00
|
|
|
headless bool
|
|
|
|
headlessCols int
|
|
|
|
headlessRows int
|
2017-10-14 11:13:47 +00:00
|
|
|
}
|
|
|
|
|
2022-11-18 10:59:56 +00:00
|
|
|
func ptyMasterNew(headless bool, headlessCols, headlessRows int) *ptyMaster {
|
|
|
|
return &ptyMaster{headless: headless, headlessCols: headlessCols, headlessRows: headlessRows}
|
2017-10-14 11:13:47 +00:00
|
|
|
}
|
|
|
|
|
2020-01-11 22:06:59 +00:00
|
|
|
func isStdinTerminal() bool {
|
|
|
|
return terminal.IsTerminal(0)
|
|
|
|
}
|
|
|
|
|
2020-12-20 18:06:06 +00:00
|
|
|
func (pty *ptyMaster) Start(command string, args []string, envVars []string) (err error) {
|
2017-10-14 11:13:47 +00:00
|
|
|
pty.command = exec.Command(command, args...)
|
2020-12-20 18:06:06 +00:00
|
|
|
pty.command.Env = envVars
|
2017-10-14 11:13:47 +00:00
|
|
|
pty.ptyFile, err = ptyDevice.Start(pty.command)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-09-26 08:41:54 +00:00
|
|
|
// Set the initial window size
|
2022-11-18 10:59:56 +00:00
|
|
|
cols, rows := pty.headlessCols, pty.headlessRows
|
|
|
|
|
|
|
|
if !pty.headless {
|
|
|
|
cols, rows, err = terminal.GetSize(0)
|
|
|
|
}
|
|
|
|
|
2020-09-26 08:41:54 +00:00
|
|
|
pty.SetWinSize(rows, cols)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-11-02 22:04:24 +00:00
|
|
|
func (pty *ptyMaster) MakeRaw() (err error) {
|
2022-11-18 10:59:56 +00:00
|
|
|
// don't do anything if running headless
|
|
|
|
if pty.headless {
|
|
|
|
return nil
|
|
|
|
}
|
2020-11-02 22:04:24 +00:00
|
|
|
|
|
|
|
// Save the initial state of the terminal, before making it RAW. Note that this terminal is the
|
|
|
|
// terminal under which the tty-share command has been started, and it's identified via the
|
|
|
|
// stdin file descriptor (0 in this case)
|
|
|
|
// We need to make this terminal RAW so that when the command (passed here as a string, a shell
|
|
|
|
// usually), is receiving all the input, including the special characters:
|
|
|
|
// so no SIGINT for Ctrl-C, but the RAW character data, so no line discipline.
|
|
|
|
// Read more here: https://www.linusakesson.net/programming/tty/
|
|
|
|
pty.terminalInitState, err = terminal.MakeRaw(int(os.Stdin.Fd()))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-09-26 08:41:54 +00:00
|
|
|
func (pty *ptyMaster) SetWinChangeCB(winChangedCB onWindowChangedCB) {
|
2022-11-18 10:59:56 +00:00
|
|
|
// Start listening for window changes if not running headless
|
|
|
|
if !pty.headless {
|
|
|
|
go onWindowChanges(func(cols, rows int) {
|
|
|
|
// TODO:policy: should the server decide here if we care about the size and set it
|
|
|
|
// right here?
|
|
|
|
pty.SetWinSize(rows, cols)
|
|
|
|
|
|
|
|
// Notify the ptyMaster user of the window changes, to be sent to the remote side
|
|
|
|
winChangedCB(cols, rows)
|
|
|
|
})
|
|
|
|
}
|
2017-10-14 11:13:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (pty *ptyMaster) GetWinSize() (int, int, error) {
|
2022-11-18 10:59:56 +00:00
|
|
|
if pty.headless {
|
|
|
|
return pty.headlessCols, pty.headlessRows, nil
|
|
|
|
} else {
|
|
|
|
return terminal.GetSize(0)
|
|
|
|
}
|
2017-10-14 11:13:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (pty *ptyMaster) Write(b []byte) (int, error) {
|
|
|
|
return pty.ptyFile.Write(b)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pty *ptyMaster) Read(b []byte) (int, error) {
|
|
|
|
return pty.ptyFile.Read(b)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pty *ptyMaster) SetWinSize(rows, cols int) {
|
|
|
|
ptyDevice.Setsize(pty.ptyFile, rows, cols)
|
|
|
|
}
|
|
|
|
|
2018-05-12 21:44:59 +00:00
|
|
|
func (pty *ptyMaster) Refresh() {
|
|
|
|
// We wanna force the app to re-draw itself, but there doesn't seem to be a way to do that
|
|
|
|
// so we fake it by resizing the window quickly, making it smaller and then back big
|
|
|
|
cols, rows, err := pty.GetWinSize()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
pty.SetWinSize(rows-1, cols)
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
time.Sleep(time.Millisecond * 50)
|
|
|
|
pty.SetWinSize(rows, cols)
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2017-10-14 11:13:47 +00:00
|
|
|
func (pty *ptyMaster) Wait() (err error) {
|
|
|
|
err = pty.command.Wait()
|
2020-10-19 17:22:12 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pty *ptyMaster) Restore() {
|
2022-11-18 10:59:56 +00:00
|
|
|
if !pty.headless {
|
|
|
|
terminal.Restore(0, pty.terminalInitState)
|
|
|
|
}
|
2017-10-14 11:13:47 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pty *ptyMaster) Stop() (err error) {
|
|
|
|
signal.Ignore(syscall.SIGWINCH)
|
|
|
|
|
|
|
|
pty.command.Process.Signal(syscall.SIGTERM)
|
|
|
|
// TODO: Find a proper wai to close the running command. Perhaps have a timeout after which,
|
|
|
|
// if the command hasn't reacted to SIGTERM, then send a SIGKILL
|
|
|
|
// (bash for example doesn't finish if only a SIGTERM has been sent)
|
|
|
|
pty.command.Process.Signal(syscall.SIGKILL)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-09-26 08:41:54 +00:00
|
|
|
func onWindowChanges(wcCB onWindowChangedCB) {
|
|
|
|
wcChan := make(chan os.Signal, 1)
|
|
|
|
signal.Notify(wcChan, syscall.SIGWINCH)
|
2017-10-14 11:13:47 +00:00
|
|
|
// The interface for getting window changes from the pty slave to its process, is via signals.
|
2019-07-20 10:50:33 +00:00
|
|
|
// In our case here, the tty-share command (built in this project) is the client, which should
|
2017-10-14 11:13:47 +00:00
|
|
|
// get notified if the terminal window in which it runs has changed. To get that, it needs to
|
|
|
|
// register for SIGWINCH signal, which is used by the kernel to tell process that the window
|
|
|
|
// has changed its dimentions.
|
|
|
|
// Read more here: https://www.linusakesson.net/programming/tty/
|
|
|
|
// Shortly, ioctl calls are used to communicate from the process to the pty slave device,
|
|
|
|
// and signals are used for the communiation in the reverse direction: from the pty slave
|
|
|
|
// device to the process.
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
2020-09-26 08:41:54 +00:00
|
|
|
case <-wcChan:
|
2017-10-14 11:13:47 +00:00
|
|
|
cols, rows, err := terminal.GetSize(0)
|
|
|
|
if err == nil {
|
2020-09-26 08:41:54 +00:00
|
|
|
wcCB(cols, rows)
|
2017-10-14 11:13:47 +00:00
|
|
|
} else {
|
|
|
|
log.Warnf("Can't get window size: %s", err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|