This commit is contained in:
Iwasaki Yudai 2017-02-26 07:37:07 +09:00
parent 54403dd678
commit a6133f34b7
54 changed files with 2140 additions and 1334 deletions

View file

@ -0,0 +1,3 @@
// Package localcommand provides an implementation of webtty.Slave
// that launches a local command with a PTY.
package localcommand

View file

@ -0,0 +1,44 @@
package localcommand
import (
"syscall"
"github.com/yudai/gotty/server"
)
type Options struct {
CloseSignal int `hcl:"close_signal" flagName:"close-signal" flagSName:"" flagDescribe:"Signal sent to the command process when gotty close it (default: SIGHUP)" default:"1"`
CloseTimeout int `hcl:"close_timeout" flagName:"close-timeout" flagSName:"" flagDescribe:"Time in seconds to force kill process after client is disconnected (default: -1)" default:"-1"`
}
type Factory struct {
command string
argv []string
options *Options
}
func NewFactory(command string, argv []string, options *Options) (*Factory, error) {
return &Factory{
command: command,
argv: argv,
options: options,
}, nil
}
func (factory *Factory) Name() string {
return "local command"
}
func (factory *Factory) New(params map[string][]string) (server.Slave, error) {
argv := make([]string, len(factory.argv))
copy(argv, factory.argv)
if params["arg"] != nil && len(params["arg"]) > 0 {
argv = append(argv, params["arg"]...)
}
return New(
factory.command,
argv,
WithCloseSignal(syscall.Signal(factory.options.CloseSignal)),
WithCloseSignal(syscall.Signal(factory.options.CloseTimeout)),
)
}

View file

@ -0,0 +1,136 @@
package localcommand
import (
"os"
"os/exec"
"syscall"
"time"
"unsafe"
"github.com/kr/pty"
"github.com/pkg/errors"
)
const (
DefaultCloseSignal = syscall.SIGINT
DefaultCloseTimeout = 10 * time.Second
)
type LocalCommand struct {
command string
argv []string
closeSignal syscall.Signal
closeTimeout time.Duration
cmd *exec.Cmd
pty *os.File
ptyClosed chan struct{}
}
func New(command string, argv []string, options ...Option) (*LocalCommand, error) {
cmd := exec.Command(command, argv...)
pty, err := pty.Start(cmd)
if err != nil {
// todo close cmd?
return nil, errors.Wrapf(err, "failed to start command `%s`", command)
}
ptyClosed := make(chan struct{})
lcmd := &LocalCommand{
command: command,
argv: argv,
closeSignal: DefaultCloseSignal,
closeTimeout: DefaultCloseTimeout,
cmd: cmd,
pty: pty,
ptyClosed: ptyClosed,
}
for _, option := range options {
option(lcmd)
}
// When the process is closed by the user,
// close pty so that Read() on the pty breaks with an EOF.
go func() {
defer func() {
lcmd.pty.Close()
close(lcmd.ptyClosed)
}()
lcmd.cmd.Wait()
}()
return lcmd, nil
}
func (lcmd *LocalCommand) Read(p []byte) (n int, err error) {
return lcmd.pty.Read(p)
}
func (lcmd *LocalCommand) Write(p []byte) (n int, err error) {
return lcmd.pty.Write(p)
}
func (lcmd *LocalCommand) Close() error {
if lcmd.cmd != nil && lcmd.cmd.Process != nil {
lcmd.cmd.Process.Signal(lcmd.closeSignal)
}
for {
select {
case <-lcmd.ptyClosed:
return nil
case <-lcmd.closeTimeoutC():
lcmd.cmd.Process.Signal(syscall.SIGKILL)
}
}
}
func (lcmd *LocalCommand) WindowTitleVariables() map[string]interface{} {
return map[string]interface{}{
"command": lcmd.command,
"argv": lcmd.argv,
"pid": lcmd.cmd.Process.Pid,
}
}
func (lcmd *LocalCommand) ResizeTerminal(width int, height int) error {
window := struct {
row uint16
col uint16
x uint16
y uint16
}{
uint16(height),
uint16(width),
0,
0,
}
_, _, errno := syscall.Syscall(
syscall.SYS_IOCTL,
lcmd.pty.Fd(),
syscall.TIOCSWINSZ,
uintptr(unsafe.Pointer(&window)),
)
if errno != 0 {
return errno
} else {
return nil
}
}
func (lcmd *LocalCommand) GetTerminalSize() (int, int, error) {
return pty.Getsize(lcmd.pty)
}
func (lcmd *LocalCommand) closeTimeoutC() <-chan time.Time {
if lcmd.closeTimeout >= 0 {
return time.After(lcmd.closeTimeout)
}
return make(chan time.Time)
}

View file

@ -0,0 +1,20 @@
package localcommand
import (
"syscall"
"time"
)
type Option func(*LocalCommand)
func WithCloseSignal(signal syscall.Signal) Option {
return func(lcmd *LocalCommand) {
lcmd.closeSignal = signal
}
}
func WithCloseTimeout(timeout time.Duration) Option {
return func(lcmd *LocalCommand) {
lcmd.closeTimeout = timeout
}
}