mirror of
https://github.com/sorenisanerd/gotty.git
synced 2024-11-22 12:24:25 +00:00
refactor: decouple gotty app with terminal backends
This commit is contained in:
parent
d71e2fcfa8
commit
496ef86339
107
app/app.go
107
app/app.go
@ -13,20 +13,18 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"text/template"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/yudai/gotty/backends"
|
||||||
"github.com/yudai/gotty/utils"
|
"github.com/yudai/gotty/utils"
|
||||||
|
|
||||||
"github.com/braintree/manners"
|
"github.com/braintree/manners"
|
||||||
"github.com/elazarl/go-bindata-assetfs"
|
"github.com/elazarl/go-bindata-assetfs"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/kr/pty"
|
|
||||||
"github.com/yudai/umutex"
|
"github.com/yudai/umutex"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -36,14 +34,12 @@ type InitMessage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
command []string
|
manager backends.ClientContextManager
|
||||||
options *Options
|
options *Options
|
||||||
|
|
||||||
upgrader *websocket.Upgrader
|
upgrader *websocket.Upgrader
|
||||||
server *manners.GracefulServer
|
server *manners.GracefulServer
|
||||||
|
|
||||||
titleTemplate *template.Template
|
|
||||||
|
|
||||||
onceMutex *umutex.UnblockingMutex
|
onceMutex *umutex.UnblockingMutex
|
||||||
timer *time.Timer
|
timer *time.Timer
|
||||||
|
|
||||||
@ -66,14 +62,12 @@ type Options struct {
|
|||||||
TLSKeyFile string `hcl:"tls_key_file" flagName:"tls-key" flagDescribe:"TLS/SSL key file path" default:"~/.gotty.key"`
|
TLSKeyFile string `hcl:"tls_key_file" flagName:"tls-key" flagDescribe:"TLS/SSL key file path" default:"~/.gotty.key"`
|
||||||
EnableTLSClientAuth bool `hcl:"enable_tls_client_auth" default:"false"`
|
EnableTLSClientAuth bool `hcl:"enable_tls_client_auth" default:"false"`
|
||||||
TLSCACrtFile string `hcl:"tls_ca_crt_file" flagName:"tls-ca-crt" flagDescribe:"TLS/SSL CA certificate file for client certifications" default:"~/.gotty.ca.crt"`
|
TLSCACrtFile string `hcl:"tls_ca_crt_file" flagName:"tls-ca-crt" flagDescribe:"TLS/SSL CA certificate file for client certifications" default:"~/.gotty.ca.crt"`
|
||||||
TitleFormat string `hcl:"title_format" flagName:"title-format" flagDescribe:"Title format of browser window" default:"GoTTY - {{ .Command }} ({{ .Hostname }})"`
|
|
||||||
EnableReconnect bool `hcl:"enable_reconnect" flagName:"reconnect" flagDescribe:"Enable reconnection" default:"false"`
|
EnableReconnect bool `hcl:"enable_reconnect" flagName:"reconnect" flagDescribe:"Enable reconnection" default:"false"`
|
||||||
ReconnectTime int `hcl:"reconnect_time" flagName:"reconnect-time" flagDescribe:"Time to reconnect" default:"10"`
|
ReconnectTime int `hcl:"reconnect_time" flagName:"reconnect-time" flagDescribe:"Time to reconnect" default:"10"`
|
||||||
MaxConnection int `hcl:"max_connection" flagName:"max-connection" flagDescribe:"Maximum connection to gotty" default:"0"`
|
MaxConnection int `hcl:"max_connection" flagName:"max-connection" flagDescribe:"Maximum connection to gotty" default:"0"`
|
||||||
Once bool `hcl:"once" flagName:"once" flagDescribe:"Accept only one client and exit on disconnection" default:"false"`
|
Once bool `hcl:"once" flagName:"once" flagDescribe:"Accept only one client and exit on disconnection" default:"false"`
|
||||||
Timeout int `hcl:"timeout" flagName:"timeout" flagDescribe:"Timeout seconds for waiting a client(0 to disable)" default:"0"`
|
Timeout int `hcl:"timeout" flagName:"timeout" flagDescribe:"Timeout seconds for waiting a client(0 to disable)" default:"0"`
|
||||||
PermitArguments bool `hcl:"permit_arguments" flagName:"permit-arguments" flagDescribe:"Permit clients to send command line arguments in URL (e.g. http://example.com:8080/?arg=AAA&arg=BBB)" default:"true"`
|
PermitArguments bool `hcl:"permit_arguments" flagName:"permit-arguments" flagDescribe:"Permit clients to send command line arguments in URL (e.g. http://example.com:8080/?arg=AAA&arg=BBB)" default:"true"`
|
||||||
CloseSignal int `hcl:"close_signal" flagName:"close-signal" flagDescribe:"Signal sent to the command process when gotty close it (default: SIGHUP)" default:"1"`
|
|
||||||
Preferences HtermPrefernces `hcl:"preferences"`
|
Preferences HtermPrefernces `hcl:"preferences"`
|
||||||
RawPreferences map[string]interface{} `hcl:"preferences"`
|
RawPreferences map[string]interface{} `hcl:"preferences"`
|
||||||
Width int `hcl:"width" flagName:"width" flagDescribe:"Static width of the screen, 0(default) means dynamically resize" default:"0"`
|
Width int `hcl:"width" flagName:"width" flagDescribe:"Static width of the screen, 0(default) means dynamically resize" default:"0"`
|
||||||
@ -82,16 +76,10 @@ type Options struct {
|
|||||||
|
|
||||||
var Version = "1.0.0"
|
var Version = "1.0.0"
|
||||||
|
|
||||||
func New(command []string, options *Options) (*App, error) {
|
func New(manager backends.ClientContextManager, options *Options) (*App, error) {
|
||||||
titleTemplate, err := template.New("title").Parse(options.TitleFormat)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("Title format string syntax error")
|
|
||||||
}
|
|
||||||
|
|
||||||
connections := int64(0)
|
connections := int64(0)
|
||||||
|
|
||||||
return &App{
|
return &App{
|
||||||
command: command,
|
manager: manager,
|
||||||
options: options,
|
options: options,
|
||||||
|
|
||||||
upgrader: &websocket.Upgrader{
|
upgrader: &websocket.Upgrader{
|
||||||
@ -99,9 +87,6 @@ func New(command []string, options *Options) (*App, error) {
|
|||||||
WriteBufferSize: 1024,
|
WriteBufferSize: 1024,
|
||||||
Subprotocols: []string{"gotty"},
|
Subprotocols: []string{"gotty"},
|
||||||
},
|
},
|
||||||
|
|
||||||
titleTemplate: titleTemplate,
|
|
||||||
|
|
||||||
onceMutex: umutex.New(),
|
onceMutex: umutex.New(),
|
||||||
connections: &connections,
|
connections: &connections,
|
||||||
}, nil
|
}, nil
|
||||||
@ -169,10 +154,6 @@ func (app *App) Run() error {
|
|||||||
if app.options.EnableTLS {
|
if app.options.EnableTLS {
|
||||||
scheme = "https"
|
scheme = "https"
|
||||||
}
|
}
|
||||||
log.Printf(
|
|
||||||
"Server is starting with command: %s",
|
|
||||||
strings.Join(app.command, " "),
|
|
||||||
)
|
|
||||||
if app.options.Address != "" {
|
if app.options.Address != "" {
|
||||||
log.Printf(
|
log.Printf(
|
||||||
"URL: %s",
|
"URL: %s",
|
||||||
@ -267,8 +248,23 @@ func (app *App) restartTimer() {
|
|||||||
|
|
||||||
func (app *App) handleWS(w http.ResponseWriter, r *http.Request) {
|
func (app *App) handleWS(w http.ResponseWriter, r *http.Request) {
|
||||||
app.stopTimer()
|
app.stopTimer()
|
||||||
|
|
||||||
connections := atomic.AddInt64(app.connections, 1)
|
connections := atomic.AddInt64(app.connections, 1)
|
||||||
|
defer func() {
|
||||||
|
connections := atomic.AddInt64(app.connections, -1)
|
||||||
|
|
||||||
|
if app.options.MaxConnection != 0 {
|
||||||
|
log.Printf("Connection closed: %s, connections: %d/%d",
|
||||||
|
r.RemoteAddr, connections, app.options.MaxConnection)
|
||||||
|
} else {
|
||||||
|
log.Printf("Connection closed: %s, connections: %d",
|
||||||
|
r.RemoteAddr, connections)
|
||||||
|
}
|
||||||
|
|
||||||
|
if connections == 0 {
|
||||||
|
app.restartTimer()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if int64(app.options.MaxConnection) != 0 {
|
if int64(app.options.MaxConnection) != 0 {
|
||||||
if connections > int64(app.options.MaxConnection) {
|
if connections > int64(app.options.MaxConnection) {
|
||||||
log.Printf("Reached max connection: %d", app.options.MaxConnection)
|
log.Printf("Reached max connection: %d", app.options.MaxConnection)
|
||||||
@ -287,11 +283,11 @@ func (app *App) handleWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Print("Failed to upgrade connection: " + err.Error())
|
log.Print("Failed to upgrade connection: " + err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
_, stream, err := conn.ReadMessage()
|
_, stream, err := conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print("Failed to authenticate websocket connection")
|
log.Print("Failed to authenticate websocket connection")
|
||||||
conn.Close()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var init InitMessage
|
var init InitMessage
|
||||||
@ -299,32 +295,34 @@ func (app *App) handleWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
err = json.Unmarshal(stream, &init)
|
err = json.Unmarshal(stream, &init)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to parse init message %v", err)
|
log.Printf("Failed to parse init message %v", err)
|
||||||
conn.Close()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if init.AuthToken != app.options.Credential {
|
if init.AuthToken != app.options.Credential {
|
||||||
log.Print("Failed to authenticate websocket connection")
|
log.Print("Failed to authenticate websocket connection")
|
||||||
conn.Close()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
argv := app.command[1:]
|
|
||||||
if app.options.PermitArguments {
|
var queryPath string
|
||||||
if init.Arguments == "" {
|
if app.options.PermitArguments && init.Arguments != "" {
|
||||||
init.Arguments = "?"
|
queryPath = init.Arguments
|
||||||
}
|
} else {
|
||||||
query, err := url.Parse(init.Arguments)
|
queryPath = "?"
|
||||||
if err != nil {
|
}
|
||||||
log.Print("Failed to parse arguments")
|
|
||||||
conn.Close()
|
query, err := url.Parse(queryPath)
|
||||||
return
|
if err != nil {
|
||||||
}
|
log.Print("Failed to parse arguments")
|
||||||
params := query.Query()["arg"]
|
return
|
||||||
if len(params) != 0 {
|
}
|
||||||
argv = append(argv, params...)
|
params := query.Query()
|
||||||
}
|
ctx, err := app.manager.New(params)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to new client context %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.server.StartRoutine()
|
app.server.StartRoutine()
|
||||||
|
defer app.server.FinishRoutine()
|
||||||
|
|
||||||
if app.options.Once {
|
if app.options.Once {
|
||||||
if app.onceMutex.TryLock() { // no unlock required, it will die soon
|
if app.onceMutex.TryLock() { // no unlock required, it will die soon
|
||||||
@ -337,30 +335,7 @@ func (app *App) handleWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(app.command[0], argv...)
|
context := &clientContext{app: app, connection: conn, writeMutex: &sync.Mutex{}, ClientContext: ctx}
|
||||||
ptyIo, err := pty.Start(cmd)
|
|
||||||
if err != nil {
|
|
||||||
log.Print("Failed to execute command")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.options.MaxConnection != 0 {
|
|
||||||
log.Printf("Command is running for client %s with PID %d (args=%q), connections: %d/%d",
|
|
||||||
r.RemoteAddr, cmd.Process.Pid, strings.Join(argv, " "), connections, app.options.MaxConnection)
|
|
||||||
} else {
|
|
||||||
log.Printf("Command is running for client %s with PID %d (args=%q), connections: %d",
|
|
||||||
r.RemoteAddr, cmd.Process.Pid, strings.Join(argv, " "), connections)
|
|
||||||
}
|
|
||||||
|
|
||||||
context := &clientContext{
|
|
||||||
app: app,
|
|
||||||
request: r,
|
|
||||||
connection: conn,
|
|
||||||
command: cmd,
|
|
||||||
pty: ptyIo,
|
|
||||||
writeMutex: &sync.Mutex{},
|
|
||||||
}
|
|
||||||
|
|
||||||
context.goHandleClient()
|
context.goHandleClient()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,29 +1,21 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/fatih/structs"
|
"github.com/fatih/structs"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/yudai/gotty/backends"
|
||||||
)
|
)
|
||||||
|
|
||||||
type clientContext struct {
|
type clientContext struct {
|
||||||
|
backends.ClientContext
|
||||||
app *App
|
app *App
|
||||||
request *http.Request
|
|
||||||
connection *websocket.Conn
|
connection *websocket.Conn
|
||||||
command *exec.Cmd
|
|
||||||
pty *os.File
|
|
||||||
writeMutex *sync.Mutex
|
writeMutex *sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,56 +38,23 @@ type argResizeTerminal struct {
|
|||||||
Rows float64
|
Rows float64
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContextVars struct {
|
|
||||||
Command string
|
|
||||||
Pid int
|
|
||||||
Hostname string
|
|
||||||
RemoteAddr string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (context *clientContext) goHandleClient() {
|
func (context *clientContext) goHandleClient() {
|
||||||
exit := make(chan bool, 2)
|
exit := make(chan bool, 3)
|
||||||
|
|
||||||
|
context.Start(exit)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer func() { exit <- true }()
|
defer func() { exit <- true }()
|
||||||
|
|
||||||
context.processSend()
|
context.processSend()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer func() { exit <- true }()
|
defer func() { exit <- true }()
|
||||||
|
|
||||||
context.processReceive()
|
context.processReceive()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
<-exit
|
||||||
defer context.app.server.FinishRoutine()
|
context.TearDown()
|
||||||
defer func() {
|
|
||||||
connections := atomic.AddInt64(context.app.connections, -1)
|
|
||||||
|
|
||||||
if context.app.options.MaxConnection != 0 {
|
|
||||||
log.Printf("Connection closed: %s, connections: %d/%d",
|
|
||||||
context.request.RemoteAddr, connections, context.app.options.MaxConnection)
|
|
||||||
} else {
|
|
||||||
log.Printf("Connection closed: %s, connections: %d",
|
|
||||||
context.request.RemoteAddr, connections)
|
|
||||||
}
|
|
||||||
|
|
||||||
if connections == 0 {
|
|
||||||
context.app.restartTimer()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
<-exit
|
|
||||||
context.pty.Close()
|
|
||||||
|
|
||||||
// Even if the PTY has been closed,
|
|
||||||
// Read(0 in processSend() keeps blocking and the process doen't exit
|
|
||||||
context.command.Process.Signal(syscall.Signal(context.app.options.CloseSignal))
|
|
||||||
|
|
||||||
context.command.Wait()
|
|
||||||
context.connection.Close()
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (context *clientContext) processSend() {
|
func (context *clientContext) processSend() {
|
||||||
@ -107,9 +66,9 @@ func (context *clientContext) processSend() {
|
|||||||
buf := make([]byte, 1024)
|
buf := make([]byte, 1024)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
size, err := context.pty.Read(buf)
|
size, err := context.OutputReader().Read(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Command exited for: %s", context.request.RemoteAddr)
|
log.Printf("failed to read output from terminal backend: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
safeMessage := base64.StdEncoding.EncodeToString([]byte(buf[:size]))
|
safeMessage := base64.StdEncoding.EncodeToString([]byte(buf[:size]))
|
||||||
@ -127,19 +86,11 @@ func (context *clientContext) write(data []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (context *clientContext) sendInitialize() error {
|
func (context *clientContext) sendInitialize() error {
|
||||||
hostname, _ := os.Hostname()
|
windowTitle, err := context.WindowTitle()
|
||||||
titleVars := ContextVars{
|
if err != nil {
|
||||||
Command: strings.Join(context.app.command, " "),
|
|
||||||
Pid: context.command.Process.Pid,
|
|
||||||
Hostname: hostname,
|
|
||||||
RemoteAddr: context.request.RemoteAddr,
|
|
||||||
}
|
|
||||||
|
|
||||||
titleBuffer := new(bytes.Buffer)
|
|
||||||
if err := context.app.titleTemplate.Execute(titleBuffer, titleVars); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := context.write(append([]byte{SetWindowTitle}, titleBuffer.Bytes()...)); err != nil {
|
if err := context.write(append([]byte{SetWindowTitle}, []byte(windowTitle)...)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +138,7 @@ func (context *clientContext) processReceive() {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := context.pty.Write(data[1:])
|
_, err := context.InputWriter().Write(data[1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -204,7 +155,6 @@ func (context *clientContext) processReceive() {
|
|||||||
log.Print("Malformed remote command")
|
log.Print("Malformed remote command")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rows := uint16(context.app.options.Height)
|
rows := uint16(context.app.options.Height)
|
||||||
if rows == 0 {
|
if rows == 0 {
|
||||||
rows = uint16(args.Rows)
|
rows = uint16(args.Rows)
|
||||||
@ -215,24 +165,7 @@ func (context *clientContext) processReceive() {
|
|||||||
columns = uint16(args.Columns)
|
columns = uint16(args.Columns)
|
||||||
}
|
}
|
||||||
|
|
||||||
window := struct {
|
context.ResizeTerminal(columns, rows)
|
||||||
row uint16
|
|
||||||
col uint16
|
|
||||||
x uint16
|
|
||||||
y uint16
|
|
||||||
}{
|
|
||||||
rows,
|
|
||||||
columns,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
}
|
|
||||||
syscall.Syscall(
|
|
||||||
syscall.SYS_IOCTL,
|
|
||||||
context.pty.Fd(),
|
|
||||||
syscall.TIOCSWINSZ,
|
|
||||||
uintptr(unsafe.Pointer(&window)),
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
log.Print("Unknown message type")
|
log.Print("Unknown message type")
|
||||||
return
|
return
|
||||||
|
19
backends/interface.go
Normal file
19
backends/interface.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package backends
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientContextManager interface {
|
||||||
|
New(params url.Values) (ClientContext, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientContext interface {
|
||||||
|
WindowTitle() (string, error)
|
||||||
|
Start(exitCh chan bool)
|
||||||
|
InputWriter() io.Writer
|
||||||
|
OutputReader() io.Reader
|
||||||
|
ResizeTerminal(width, height uint16) error
|
||||||
|
TearDown() error
|
||||||
|
}
|
BIN
backends/ptycommand/.command.go.swp
Normal file
BIN
backends/ptycommand/.command.go.swp
Normal file
Binary file not shown.
130
backends/ptycommand/command.go
Normal file
130
backends/ptycommand/command.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package ptycommand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
"text/template"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/yudai/gotty/backends"
|
||||||
|
|
||||||
|
"github.com/kr/pty"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"`
|
||||||
|
TitleFormat string `hcl:"title_format" flagName:"title-format" flagSName:"" flagDescribe:"Title format of browser window" default:"GoTTY - {{ .Command }} ({{ .Hostname }})"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandClientContextManager struct {
|
||||||
|
command []string
|
||||||
|
|
||||||
|
options *Options
|
||||||
|
titleTemplate *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommandClientContextManager(command []string, options *Options) (*CommandClientContextManager, error) {
|
||||||
|
titleTemplate, err := template.New("title").Parse(options.TitleFormat)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Title format string syntax error: %v", options.TitleFormat)
|
||||||
|
}
|
||||||
|
return &CommandClientContextManager{command: command, options: options, titleTemplate: titleTemplate}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandClientContext struct {
|
||||||
|
cmd *exec.Cmd
|
||||||
|
pty *os.File
|
||||||
|
mgr *CommandClientContextManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mgr *CommandClientContextManager) New(params url.Values) (backends.ClientContext, error) {
|
||||||
|
argv := mgr.command[1:]
|
||||||
|
args := params["arg"]
|
||||||
|
if len(args) != 0 {
|
||||||
|
argv = append(argv, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(mgr.command[0], argv...)
|
||||||
|
return &CommandClientContext{cmd: cmd, mgr: mgr}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (context *CommandClientContext) WindowTitle() (title string, err error) {
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
|
||||||
|
titleVars := struct {
|
||||||
|
Command string
|
||||||
|
Pid int
|
||||||
|
Hostname string
|
||||||
|
}{
|
||||||
|
Command: context.cmd.Path,
|
||||||
|
Pid: context.cmd.Process.Pid,
|
||||||
|
Hostname: hostname,
|
||||||
|
}
|
||||||
|
|
||||||
|
titleBuffer := new(bytes.Buffer)
|
||||||
|
if err := context.mgr.titleTemplate.Execute(titleBuffer, titleVars); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return titleBuffer.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (context *CommandClientContext) Start(exitCh chan bool) {
|
||||||
|
ptyIo, err := pty.Start(context.cmd)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to start command %v", err)
|
||||||
|
exitCh <- true
|
||||||
|
} else {
|
||||||
|
context.pty = ptyIo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (context *CommandClientContext) InputWriter() io.Writer {
|
||||||
|
return context.pty
|
||||||
|
}
|
||||||
|
|
||||||
|
func (context *CommandClientContext) OutputReader() io.Reader {
|
||||||
|
return context.pty
|
||||||
|
}
|
||||||
|
|
||||||
|
func (context *CommandClientContext) ResizeTerminal(width, height uint16) error {
|
||||||
|
window := struct {
|
||||||
|
row uint16
|
||||||
|
col uint16
|
||||||
|
x uint16
|
||||||
|
y uint16
|
||||||
|
}{
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
}
|
||||||
|
_, _, errno := syscall.Syscall(
|
||||||
|
syscall.SYS_IOCTL,
|
||||||
|
context.pty.Fd(),
|
||||||
|
syscall.TIOCSWINSZ,
|
||||||
|
uintptr(unsafe.Pointer(&window)),
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
return errno
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (context *CommandClientContext) TearDown() error {
|
||||||
|
context.pty.Close()
|
||||||
|
|
||||||
|
// Even if the PTY has been closed,
|
||||||
|
// Read(0 in processSend() keeps blocking and the process doen't exit
|
||||||
|
if context.cmd != nil && context.cmd.Process != nil {
|
||||||
|
context.cmd.Process.Signal(syscall.Signal(context.mgr.options.CloseSignal))
|
||||||
|
context.cmd.Wait()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
36
main.go
36
main.go
@ -9,22 +9,28 @@ import (
|
|||||||
"github.com/codegangsta/cli"
|
"github.com/codegangsta/cli"
|
||||||
|
|
||||||
"github.com/yudai/gotty/app"
|
"github.com/yudai/gotty/app"
|
||||||
|
"github.com/yudai/gotty/backends/ptycommand"
|
||||||
"github.com/yudai/gotty/utils"
|
"github.com/yudai/gotty/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cmd := cli.NewApp()
|
cmd := cli.NewApp()
|
||||||
cmd.Version = app.Version
|
|
||||||
cmd.Name = "gotty"
|
cmd.Name = "gotty"
|
||||||
|
cmd.Version = app.Version
|
||||||
cmd.Usage = "Share your terminal as a web application"
|
cmd.Usage = "Share your terminal as a web application"
|
||||||
cmd.HideHelp = true
|
cmd.HideHelp = true
|
||||||
|
cli.AppHelpTemplate = helpTemplate
|
||||||
|
|
||||||
options := &app.Options{}
|
appOptions := &app.Options{}
|
||||||
if err := utils.ApplyDefaultValues(options); err != nil {
|
if err := utils.ApplyDefaultValues(appOptions); err != nil {
|
||||||
|
exit(err, 1)
|
||||||
|
}
|
||||||
|
backendOptions := &ptycommand.Options{}
|
||||||
|
if err := utils.ApplyDefaultValues(backendOptions); err != nil {
|
||||||
exit(err, 1)
|
exit(err, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
cliFlags, flagMappings, err := utils.GenerateFlags(options)
|
cliFlags, flagMappings, err := utils.GenerateFlags(appOptions, backendOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
exit(err, 3)
|
exit(err, 3)
|
||||||
}
|
}
|
||||||
@ -41,28 +47,33 @@ func main() {
|
|||||||
|
|
||||||
cmd.Action = func(c *cli.Context) {
|
cmd.Action = func(c *cli.Context) {
|
||||||
if len(c.Args()) == 0 {
|
if len(c.Args()) == 0 {
|
||||||
|
msg := "Error: No command given."
|
||||||
cli.ShowAppHelp(c)
|
cli.ShowAppHelp(c)
|
||||||
exit(fmt.Errorf("Error: No command given."), 1)
|
exit(fmt.Errorf(msg), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
configFile := c.String("config")
|
configFile := c.String("config")
|
||||||
_, err := os.Stat(utils.ExpandHomeDir(configFile))
|
_, err := os.Stat(utils.ExpandHomeDir(configFile))
|
||||||
if configFile != "~/.gotty" || !os.IsNotExist(err) {
|
if configFile != "~/.gotty" || !os.IsNotExist(err) {
|
||||||
if err := utils.ApplyConfigFile(configFile, options); err != nil {
|
if err := utils.ApplyConfigFile(configFile, appOptions, backendOptions); err != nil {
|
||||||
exit(err, 2)
|
exit(err, 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.ApplyFlags(cliFlags, flagMappings, c, options)
|
utils.ApplyFlags(cliFlags, flagMappings, c, appOptions, backendOptions)
|
||||||
|
|
||||||
options.EnableBasicAuth = c.IsSet("credential")
|
appOptions.EnableBasicAuth = c.IsSet("credential")
|
||||||
options.EnableTLSClientAuth = c.IsSet("tls-ca-crt")
|
appOptions.EnableTLSClientAuth = c.IsSet("tls-ca-crt")
|
||||||
|
|
||||||
if err := app.CheckConfig(options); err != nil {
|
if err := app.CheckConfig(appOptions); err != nil {
|
||||||
exit(err, 6)
|
exit(err, 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := app.New(c.Args(), options)
|
manager, err := ptycommand.NewCommandClientContextManager(c.Args(), backendOptions)
|
||||||
|
if err != nil {
|
||||||
|
exit(err, 3)
|
||||||
|
}
|
||||||
|
app, err := app.New(manager, appOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
exit(err, 3)
|
exit(err, 3)
|
||||||
}
|
}
|
||||||
@ -74,9 +85,6 @@ func main() {
|
|||||||
exit(err, 4)
|
exit(err, 4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.AppHelpTemplate = helpTemplate
|
|
||||||
|
|
||||||
cmd.Run(os.Args)
|
cmd.Run(os.Args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user