From acacba6f031de8a5334dc2c0cb82ce99babeb986 Mon Sep 17 00:00:00 2001 From: Iwasaki Yudai Date: Mon, 24 Aug 2015 07:14:24 +0900 Subject: [PATCH] Support auto reconnection A new option `--auto-reconnect` which takes seconds to reconnect is added. --- README.md | 2 +- app/app.go | 17 +++---- app/client_context.go | 18 ++++++-- app/resource.go | 4 +- main.go | 7 +++ resources/gotty.js | 104 +++++++++++++++++++++++++----------------- 6 files changed, 97 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 2a007d8..998efca 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ By default, gotty starts a web server at port 8080. Open the URL on your web bro --random-url, -r Add a random string to the URL [$GOTTY_RANDOM_URL] --profile-file, -f "~/.gotty" Path to profile file [$GOTTY_PROFILE_FILE] --title-format "GoTTY - {{ .Command }} ({{ .Hostname }})" Title format of browser window [$GOTTY_TITLE_FORMAT] ---version, -v print the version +--auto-reconnect "-1" Seconds to automatically reconnect to the server when the connection is closed (default: disabled) [$GOTTY_AUTO_RECONNECT] ``` By default, gotty doesn't allow clients to send any keystrokes or commands except terminal window resizing. When you want to permit clients to write input to the PTY, add the `-w` option. However, accepting input from remote clients is dangerous for most commands. Make sure that only trusted clients can connect to your gotty server when you activate this option. If you need interaction with the PTY, consider starting gotty with tmux or GNU Screen and run your main command on it. diff --git a/app/app.go b/app/app.go index bbc2074..9843ab6 100644 --- a/app/app.go +++ b/app/app.go @@ -32,14 +32,15 @@ type App struct { } type Options struct { - Address string - Port string - PermitWrite bool - Credential string - RandomUrl bool - ProfileFile string - TitleFormat string - Command []string + Address string + Port string + PermitWrite bool + Credential string + RandomUrl bool + ProfileFile string + TitleFormat string + AutoReconnect int + Command []string } const DefaultProfileFilePath = "~/.gotty" diff --git a/app/client_context.go b/app/client_context.go index e5e2045..5ddae7e 100644 --- a/app/client_context.go +++ b/app/client_context.go @@ -28,9 +28,10 @@ const ( ) const ( - Output = '0' - SetWindowTitle = '1' - SetPreferences = '2' + Output = '0' + SetWindowTitle = '1' + SetPreferences = '2' + SetAutoReconnect = '3' ) type argResizeTerminal struct { @@ -124,6 +125,17 @@ func (context *clientContext) sendInitialize() error { writer.Write(prefs) writer.Close() + if context.app.options.AutoReconnect >= 0 { + autoReconnect, _ := json.Marshal(context.app.options.AutoReconnect) + writer, err = context.connection.NextWriter(websocket.TextMessage) + if err != nil { + return err + } + writer.Write([]byte{SetAutoReconnect}) + writer.Write(autoReconnect) + writer.Close() + } + return nil } diff --git a/app/resource.go b/app/resource.go index a655e46..e629964 100644 --- a/app/resource.go +++ b/app/resource.go @@ -71,7 +71,7 @@ func (fi bindataFileInfo) Sys() interface{} { return nil } -var _staticGottyJs = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x8c\x54\xc1\x8e\x9b\x30\x10\xbd\xe7\x2b\x2c\x2e\x31\x6a\xe4\xdd\xf4\xd0\x43\xa2\x55\x0f\xab\xed\xa1\xad\xba\x55\x93\x76\x0f\xab\x3d\x18\x33\x09\x6e\x1c\x1b\xd9\x66\x51\x5a\xf1\xef\x1d\x13\x48\x08\x61\x51\xe7\x80\x60\xfc\xde\xf3\xf3\xcc\x18\xba\x29\xb4\xf0\xd2\x68\x1a\x93\xbf\x13\x82\xf1\xca\x2d\xc9\xbc\xcf\xdd\x83\xe6\x89\x82\x94\xdc\x91\x52\xea\xd4\x94\x4c\x19\xc1\x03\x94\xe5\xd6\x78\x23\x8c\x22\x77\x77\x24\xaa\xb1\x8b\x68\x79\x22\x17\x16\x17\x08\xbd\xd0\xf8\x48\xa6\xa5\x73\x8b\x9b\x9b\x29\x59\x84\xd7\xf0\x16\x93\x77\x57\xca\x99\x71\x7e\x20\x9d\x73\x9f\x69\xbe\x07\x5c\x42\xf2\xf4\xbc\x57\xeb\xc4\xe1\x8e\xcf\xd1\xd6\x78\x7f\x88\x5e\xce\xcb\x65\xc8\x6b\x28\xc9\x13\x24\x2b\x23\x76\xe0\x29\xba\x9b\x9d\x69\xf1\x72\x72\x02\x7b\xb0\xfb\xe6\xb3\x74\xcc\x68\x93\x83\x46\xfa\xa9\x40\xf0\x0a\xda\xb7\x55\x0a\x91\x05\x06\x4b\x61\xc3\x0b\xe5\x57\xde\x58\xbe\x85\x66\x3f\x25\x13\xd6\x64\xd8\x57\x3c\x85\xa2\xf1\x72\x94\xc7\x84\x02\x6e\x69\xeb\x27\x44\x40\x35\x72\x47\xc6\x1a\x1f\x52\x1f\xb5\x2e\x50\x68\xb6\x5d\xfb\x01\x3c\x3d\x74\x5d\x77\x0d\xb7\x27\x95\x06\x11\x35\x51\x1a\x96\x17\x2e\xbb\x50\x0c\x81\x79\xa3\x7f\xad\xbf\xc0\xc1\x79\x6b\x76\xd0\x55\xc4\x4c\x5f\xb4\xa9\x99\x03\x9d\xd2\xe8\x36\xc2\x36\x05\xd0\xf2\x02\x53\x5d\x6f\x11\xf0\x2b\x6f\xa5\xde\xa2\x7e\x7f\xcb\x21\x47\xe7\x53\x3a\xf9\xe7\xc2\x14\x36\xb3\xd8\x6b\x37\x23\xd6\x94\x6e\xcc\xde\xd5\x42\x88\x68\x1e\x3c\x7f\x5e\x3d\x7e\x63\xae\xf6\x23\x37\x87\x61\x64\x88\x6b\xf1\x6e\x34\x4e\x16\xed\xcb\x6c\x14\x1d\xec\x2e\xea\xe7\xdb\xb8\x6a\x70\x25\xbe\xca\xc6\xa3\xf5\x3e\xf6\x5b\x3b\xcf\x95\xc2\x22\x27\x86\xdb\xb4\x3b\x95\x55\x7f\xa8\x52\x10\x38\x9a\x1e\x68\x6a\x44\xb1\xc7\xd9\x67\x89\x49\x0f\x0d\xa3\xea\xde\x94\x3d\x38\x77\x9c\xfd\xb7\x2f\x4b\xca\x3d\x47\x40\x9d\x67\xe1\x83\x39\x25\x05\xd0\x79\xc7\x82\x2b\xa5\x17\x19\x3d\x63\x9e\x6f\x5f\xba\x1a\x82\x3b\x20\xd3\xdb\xe9\x62\xe0\x60\x86\x95\x56\x7a\xf8\xb9\xfe\x34\xff\x40\x03\xb7\x37\x7e\x89\x05\xbe\x5b\xf6\xa4\xe6\x43\x52\x0e\xfc\x53\xfd\xff\x59\x4b\xaf\xe0\xbf\xb5\xde\xf7\xb4\x72\x0b\x1b\xb0\xa0\x05\x84\x9f\x50\x3d\x5b\x39\xb7\x6e\x50\xf0\x31\xf9\x0d\xc2\xb3\x1d\xce\x3e\xed\xf0\x62\xb6\x31\xf6\x81\x63\x49\x4e\x75\x45\xc8\xd0\x74\xd7\xc6\xb7\xe0\xbf\x23\xd9\xd1\x38\x9c\x21\x40\x67\x5d\x17\xcf\x98\x78\xe9\x5f\xca\xf1\x93\x1d\x27\xaf\xea\xb4\x5a\x28\xe3\xc6\x1b\xdd\xb6\xc3\x65\xa6\x7c\x7c\x05\xab\xf8\x81\x46\xf7\x46\x6b\xa8\x09\xe4\x3e\x28\xa4\xd1\x8c\xe8\x42\xa9\xce\xf6\x35\xaf\xd0\x6f\x4c\x68\x35\xa9\x62\x1a\x4f\xfe\x05\x00\x00\xff\xff\x0d\xc1\x26\x9b\xaf\x06\x00\x00") +var _staticGottyJs = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x94\x55\xc1\x6e\x1a\x31\x10\xbd\xe7\x2b\xac\xbd\xe0\x6d\xa9\x03\xad\xd4\x03\x88\xf6\x10\xa5\x87\xb6\x6a\xaa\x40\x9b\x43\x94\x83\xf1\x0e\xe0\x62\x6c\x64\x7b\xb3\xa2\x15\xff\xde\xf1\xb2\xc0\xb2\x78\x83\xe2\x03\x5a\x3c\x6f\xde\xcc\x1b\xcf\xd8\x74\x96\x6b\xe1\xa5\xd1\x34\x25\xff\xae\x08\xae\x67\x6e\xc9\xc2\xfb\xb5\xbb\xd5\x7c\xaa\x20\x23\x23\x52\x48\x9d\x99\x82\x29\x23\x78\x80\xb2\xb5\x35\xde\x08\xa3\xc8\x68\x44\x92\x12\x3b\x48\x86\x07\xe7\xdc\xa2\x81\xd0\x13\x8e\xcf\xa4\x53\x38\x37\xb8\xbe\xee\x90\x41\xf8\x0c\x5f\x29\x79\x7b\xc6\xbc\x30\xce\x47\xb6\xd7\xdc\x2f\x34\x5f\x01\x9a\xd0\xb9\x73\x8c\xb5\xcf\xc4\x61\xc4\xc7\x64\x6e\xbc\xdf\x24\x4f\x47\x33\xcf\xbd\xb9\x07\x61\xb4\x06\xe1\x11\xf2\xae\x3f\xbc\x3a\x18\xcd\x1a\xf4\x43\x70\x3c\x2b\xc1\x1e\x51\x04\xab\x86\x82\x3c\xc0\x74\x6c\xc4\x12\x3c\x45\x71\xdd\x63\xd4\xb4\xa2\xdb\x3b\x78\xb0\xab\xda\x56\xe1\x98\xd1\x21\x4c\x3d\x08\x3c\x83\xf6\xf5\x48\x61\x2d\x82\x27\xcb\x60\xc6\x73\xe5\xc7\xde\x58\x3e\x87\x2a\xb6\x92\x53\x56\xed\xb0\xef\x58\x10\x45\xd3\xe1\x45\x5f\x26\x14\x70\x4b\xeb\xf9\x85\x15\x90\x15\xed\xce\x6b\x82\x3f\x52\xef\x38\xcf\x90\x98\xfc\xde\x7e\x0f\x3c\xdb\xb4\x95\xaa\x5e\x01\x69\x10\x55\x3a\x4b\xc3\xd6\xb9\x5b\x9c\x31\x87\x85\x36\xa3\x7f\x4f\xbe\xc1\xc6\x79\x6b\x96\x50\x67\xc6\x9d\x18\x79\x55\x4f\x07\x3a\xa3\x49\x2f\xc1\x4e\x08\xc0\xe1\x19\x6e\x1b\x0f\x17\xfc\xc6\xde\x4a\x3d\xc7\x58\xcd\xf0\x6d\x19\x1e\xd5\x3b\xf9\xf7\x24\x49\x3c\xfc\x7c\xa5\x5d\x97\x58\x53\xb8\x4b\xe9\x46\x8d\x61\x25\xfd\xa0\xe3\xeb\xf8\xee\x07\x73\x65\x6e\x72\xb6\x69\x47\x87\x15\x0f\x54\x5f\x55\x66\x83\xfd\x47\xf7\xa2\x47\x90\x30\x28\x7f\x5f\xc6\x6e\x5b\xad\x69\xd4\x72\xbe\x1b\x3b\x9b\x5d\xaf\x68\xe7\xb9\x52\x78\x20\x53\xc3\x6d\xd6\xec\xf0\xa6\x5f\xd5\xf0\x02\x5b\xdd\x03\xcd\x8c\xc8\x57\x38\x53\x6c\x6a\xb2\x4d\xcd\x73\xdb\x9c\xc4\x15\x38\xb7\x9b\xab\x97\x87\x31\xe3\x9e\x23\xa8\xb4\xb1\xf0\x87\x39\x25\x05\xd0\x7e\x23\x2d\x57\x48\x2f\x16\xf4\x88\x7b\xec\x3d\x35\xb9\x04\x77\x40\x3a\xbd\xce\xa0\x45\xb8\x61\x85\x95\x1e\x7e\x4d\xbe\xf4\x3f\xd2\xc0\x11\x69\xe9\xa9\x05\xbe\x1c\x46\x68\xfb\x6d\xb4\x0e\xfc\x43\x79\x85\x4e\xa4\x57\xf0\x6a\xde\xf7\x11\xde\xb5\x85\x19\x58\xd0\x02\xc2\xa5\x58\xf6\xec\x9a\x5b\xd7\x4a\x7e\x37\xfd\x83\x77\x2e\x5b\xe2\x8c\xd1\x9a\x6f\xca\x66\xc6\xde\x72\x2c\xdb\xe1\x0c\x10\xd2\x36\x41\xa5\x98\x39\xf8\x9f\x48\xe0\x68\x1a\x74\x05\x78\xb7\x9e\xcd\x23\x6e\x3c\xc5\x2e\x82\xd7\x28\xfe\x10\x51\xdc\x7c\x3c\x2e\x6b\x8e\xd0\x1f\x87\x66\xdb\xe8\x46\xa1\x8c\xbb\xdc\x8b\x72\x46\x68\xa8\x42\xac\x42\x65\x75\x72\x7d\x61\x78\x0e\xc8\x70\x0b\x2e\x4c\x71\xf7\x0c\x56\xf1\x0d\x4d\x6e\x76\xca\x30\x34\xb9\x09\xb9\x64\x49\x97\xe8\x5c\xa9\xe6\xf0\x9d\x8e\x9e\xdd\x1c\x6a\x52\x8f\xd4\x54\x07\xd6\x1a\x7b\xa2\x2e\x6c\x34\x55\xb4\xb3\xd5\x38\x77\xef\xf4\xe1\x1d\x29\xdf\xd9\x9a\x5f\xdb\xab\x14\x2a\x77\x7a\x84\x9f\x46\xa4\xd7\xcc\x00\x3b\x6a\x22\x57\x60\x72\x4f\x77\x71\xba\x8d\x73\x7f\x43\xfa\xbd\x5e\x2f\x92\xdb\x36\xa5\xe9\xd5\xff\x00\x00\x00\xff\xff\x1e\x3d\x0c\xe8\x3f\x09\x00\x00") func staticGottyJsBytes() ([]byte, error) { return bindataRead( @@ -86,7 +86,7 @@ func staticGottyJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static/gotty.js", size: 1711, mode: os.FileMode(436), modTime: time.Unix(1440336972, 0)} + info := bindataFileInfo{name: "static/gotty.js", size: 2367, mode: os.FileMode(436), modTime: time.Unix(1440367388, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/main.go b/main.go index 33e1909..451b141 100644 --- a/main.go +++ b/main.go @@ -55,6 +55,12 @@ func main() { Usage: "Title format of browser window", EnvVar: "GOTTY_TITLE_FORMAT", }, + cli.IntFlag{ + Name: "auto-reconnect", + Value: -1, + Usage: "Seconds to automatically reconnect to the server when the connection is closed (default: disabled)", + EnvVar: "GOTTY_AUTO_RECONNECT", + }, } cmd.Action = func(c *cli.Context) { if len(c.Args()) == 0 { @@ -72,6 +78,7 @@ func main() { c.Bool("random-url"), c.String("profile-file"), c.String("title-format"), + c.Int("auto-reconnect"), c.Args(), }, ) diff --git a/resources/gotty.js b/resources/gotty.js index bdca523..6cae0ab 100644 --- a/resources/gotty.js +++ b/resources/gotty.js @@ -2,62 +2,84 @@ var httpsEnabled = window.location.protocol == "https:"; var url = (httpsEnabled ? 'wss://' : 'ws://') + window.location.host + window.location.pathname + 'ws'; var protocols = ["gotty"]; - var ws = new WebSocket(url, protocols); + var autoReconnect = -1; - var term; + var openWs = function() { + var ws = new WebSocket(url, protocols); - ws.onopen = function(event) { - hterm.defaultStorage = new lib.Storage.Local(); - hterm.defaultStorage.clear(); + var term; - term = new hterm.Terminal(); + ws.onopen = function(event) { + hterm.defaultStorage = new lib.Storage.Local(); + hterm.defaultStorage.clear(); - term.onTerminalReady = function() { - var io = term.io.push(); + term = new hterm.Terminal(); - io.onVTKeystroke = function(str) { - ws.send("0" + str); - }; + term.onTerminalReady = function() { + var io = term.io.push(); - io.sendString = io.onVTKeystroke; + io.onVTKeystroke = function(str) { + ws.send("0" + str); + }; - io.onTerminalResize = function(columns, rows) { - ws.send( - "1" + JSON.stringify( - { - columns: columns, - rows: rows, - } + io.sendString = io.onVTKeystroke; + + io.onTerminalResize = function(columns, rows) { + ws.send( + "1" + JSON.stringify( + { + columns: columns, + rows: rows, + } + ) ) - ) + }; + + term.installKeyboard(); }; - term.installKeyboard(); + term.decorate(document.body); }; - term.decorate(document.body); - }; + ws.onmessage = function(event) { + data = event.data.slice(1); + switch(event.data[0]) { + case '0': + term.io.writeUTF16(data); + break; + case '1': + term.setWindowTitle(data); + break; + case '2': + preferences = JSON.parse(data); + Object.keys(preferences).forEach(function(key) { + term.getPrefs().set(key, preferences[key]); + }); + break; + case '3': + autoReconnect = JSON.parse(data); + break; + } + } - ws.onmessage = function(event) { - data = event.data.slice(1); - switch(event.data[0]) { - case '0': - term.io.writeUTF16(data); - break; - case '1': - term.setWindowTitle(data); - break; - case '2': - preferences = JSON.parse(data); - Object.keys(preferences).forEach(function(key) { - term.getPrefs().set(key, preferences[key]); - }); - break; + ws.onclose = function(event) { + if (term) { + term.uninstallKeyboard(); + term.io.showOverlay("Connection Closed", null); + } + tryReconnect(); + } + + ws.onerror = function(error) { + tryReconnect(); } } - ws.onclose = function(event) { - term.io.showOverlay("Connection Closed", null); - term.uninstallKeyboard(); + openWs(); + + var tryReconnect = function() { + if (autoReconnect >= 0) { + setTimeout(openWs, autoReconnect * 1000); + } } })()