package app
import (
"fmt"
"os"
"sync"
"syscall"
"time"
"unsafe"
"github.com/barnettZQG/gotty/server"
"github.com/kr/pty"
"github.com/sirupsen/logrus"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
)
type execContext struct {
tty, pty *os.File
kubeRequest *restclient.Request
config *restclient.Config
sizeUpdate chan remotecommand.TerminalSize
closed bool
mu sync.Mutex
streamReady chan error
}
func NewExecContext(kubeRequest *restclient.Request, config *restclient.Config) (server.Slave, error) {
pty, tty, err := pty.Open()
if err != nil {
logrus.Errorf("open pty failure %s", err.Error())
return nil, err
}
ec := &execContext{
tty: tty,
pty: pty,
kubeRequest: kubeRequest,
config: config,
sizeUpdate: make(chan remotecommand.TerminalSize, 2),
streamReady: make(chan error, 1),
}
if err := ec.Run(); err != nil {
tty.Close()
pty.Close()
return nil, err
}
timeout := time.NewTimer(5 * time.Second)
defer timeout.Stop()
select {
case err := <-ec.streamReady:
if err != nil {
tty.Close()
pty.Close()
return nil, fmt.Errorf("stream failed to start: %w", err)
}
return ec, nil
case <-timeout.C:
logrus.Warnf("stream did not signal ready within timeout, proceeding anyway")
return ec, nil
}
}
func (e *execContext) WaitingStop() bool {
if e.closed {
return false
}
return true
}
func (e *execContext) Run() error {
exec, err := remotecommand.NewSPDYExecutor(e.config, "POST", e.kubeRequest.URL())
if err != nil {
return fmt.Errorf("create executor failure %s", err.Error())
}
go func() {
out := CreateOut(e.tty)
t := out.SetTTY()
streamStarted := make(chan struct{})
go func() {
time.Sleep(50 * time.Millisecond)
select {
case e.streamReady <- nil:
default:
}
close(streamStarted)
}()
t.Safe(func() error {
defer e.Close()
if err := exec.Stream(remotecommand.StreamOptions{
Stdin: out.Stdin,
Stdout: out.Stdout,
Stderr: nil,
Tty: true,
TerminalSizeQueue: e,
}); err != nil {
logrus.Errorf("executor stream failure %s", err.Error())
select {
case e.streamReady <- err:
case <-streamStarted:
}
return err
}
return nil
})
}()
return nil
}
func (e *execContext) Read(p []byte) (n int, err error) {
return e.pty.Read(p)
}
func (e *execContext) Write(p []byte) (n int, err error) {
return e.pty.Write(p)
}
func (e *execContext) Close() error {
e.mu.Lock()
defer e.mu.Unlock()
if e.closed {
return nil
}
e.closed = true
var ttyErr, ptyErr error
if e.tty != nil {
ttyErr = e.tty.Close()
}
if e.pty != nil {
ptyErr = e.pty.Close()
}
if ttyErr != nil {
return ttyErr
}
return ptyErr
}
func (e *execContext) WindowTitleVariables() map[string]interface{} {
return map[string]interface{}{}
}
func (e *execContext) Next() *remotecommand.TerminalSize {
size, ok := <-e.sizeUpdate
if !ok {
return nil
}
logrus.Infof("width %d height %d", size.Width, size.Height)
return &size
}
func (e *execContext) ResizeTerminal(width int, height int) error {
logrus.Infof("set width %d height %d", width, height)
e.sizeUpdate <- remotecommand.TerminalSize{
Width: uint16(width),
Height: uint16(height),
}
window := struct {
row uint16
col uint16
x uint16
y uint16
}{
uint16(height),
uint16(width),
0,
0,
}
_, _, errno := syscall.Syscall(
syscall.SYS_IOCTL,
e.pty.Fd(),
syscall.TIOCSWINSZ,
uintptr(unsafe.Pointer(&window)),
)
if errno != 0 {
return errno
}
return nil
}