Created
August 23, 2016 19:07
-
-
Save alfredkrohmer/1a94e3ba3db80820a4cbe19a0ab19b9a to your computer and use it in GitHub Desktop.
Web Terminal for Docker container with Sinatra and Websockets
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'sinatra' | |
require 'sinatra-websocket' | |
require 'docker-api' | |
require 'pty' | |
set :server, 'thin' | |
def new_state | |
{ | |
lock: Mutex.new, | |
cond: ConditionVariable.new, | |
fin: false | |
} | |
end | |
def notify(state) | |
state[:lock].synchronize do | |
state[:fin] = true | |
state[:cond].signal | |
end | |
end | |
def wait(state) | |
state[:lock].synchronize do | |
unless state[:fin] | |
state[:cond].wait(state[:lock]) | |
end | |
end | |
end | |
class WebSocketConnection < EventMachine::Connection | |
def initialize(ws, state, container) | |
@ws = ws | |
@state = state | |
@container = container | |
end | |
def receive_data(data) | |
@ws.send(data.force_encoding('utf-8')) | |
rescue | |
# exception kill the EventMachine | |
end | |
def unbind | |
@ws.close_websocket | |
rescue | |
# exception kill the EventMachine | |
ensure | |
notify(@state) | |
@container.delete force: true | |
end | |
end | |
class Docker::Container | |
def resize(rows, cols) | |
connection.post(path_for(:resize), h: rows, w: cols) | |
end | |
def attach_websocket(ws) | |
opts = { | |
tty: true, | |
stdin: true, | |
stream: true, | |
stdout: true, | |
stderr: true | |
} | |
excon_params = { | |
hijack_block: lambda do |socket| | |
state = new_state | |
# attach to Docker TCP connection and write everything to web socket | |
EventMachine.next_tick { EventMachine.attach(socket, WebSocketConnection, ws, state, self) } | |
# react on web socket events | |
ws.onmessage do |msg| | |
begin | |
case msg[0] | |
when 'd' | |
# regular data | |
socket.write(msg[1..-1]) | |
when 'r' | |
# terminal resize | |
resize *msg[1..-1].split(',', 2).map { |dim| dim.to_i } | |
end | |
rescue | |
# exception kill the EventMachine | |
end | |
end | |
ws.onclose do | |
begin | |
socket.close_write | |
rescue | |
# exception kill the EventMachine | |
end | |
end | |
EventMachine.next_tick { yield } if block_given? | |
Kernel.send(:wait, state) | |
end | |
} | |
Thread.start do | |
connection.post( | |
path_for(:attach), | |
opts, | |
excon_params | |
) | |
puts 'fin' | |
end | |
end | |
end | |
def run_interactive_command_in_image(ws, image, cmd, binds = {}, opts = {}) | |
opts = { | |
Image: image, | |
Entrypoint: cmd[0], | |
Cmd: cmd[1..-1], | |
HostConfig: { | |
Binds: binds.map { |from, to| "#{from}:#{to}" } | |
}, | |
Tty: true, | |
OpenStdin: true | |
}.merge opts | |
c = Docker::Container.create(opts) | |
c.attach_websocket ws do | |
c.start | |
end | |
end | |
get '/' do | |
erb :index | |
end | |
get '/terminal' do | |
if !request.websocket? | |
erb :terminal | |
else | |
request.websocket do |ws| | |
ws.onopen do | |
run_interactive_command_in_image ws, 'busybox', ['/bin/sh'] | |
end | |
end | |
end | |
end | |
__END__ | |
@@ index | |
<!DOCTYPE html> | |
<html> | |
<body> | |
<a href="javascript:window.open('/terminal','Terminal','width=600,height=400')">open terminal</a> | |
</body> | |
</html> | |
@@ terminal | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<style> | |
body {margin: 0;} | |
</style> | |
<script language="javascript" type="text/javascript" src="hterm_all.js"></script> | |
<script language="javascript" type="text/javascript" src="https://code.jquery.com/jquery-3.1.0.js"></script> | |
<script> | |
hterm.defaultStorage = new lib.Storage.Memory(); | |
$(document).ready(function() { | |
// establish WebSocket connection | |
var socket = new WebSocket('ws://' + window.location.hostname + ':8080/terminal'); | |
socket.onopen = function() { | |
// set up hterm | |
var terminal = new hterm.Terminal(); | |
socket.onmessage = function(msg) { | |
// directly print data received from the socket | |
terminal.io.print(msg.data); | |
}; | |
socket.onclose = function(msg) { | |
// notify user | |
terminal.io.print("\r\nConnection to server lost."); | |
}; | |
terminal.onTerminalReady = function() { | |
var io = terminal.io.push(); | |
// keypress hook | |
io.onVTKeystroke = io.sendString = function(str) { | |
if (socket.readyState == 1) { | |
// prefix with 'd' to indicate data | |
socket.send('d' + str); | |
} | |
}; | |
// resize hook | |
io.onTerminalResize = function(cols, rows) { | |
if (socket.readyState == 1) { | |
// prefix with 'r' to indicate resize | |
socket.send('r' + rows + ',' + cols); | |
} | |
}; | |
}; | |
// actually install hterm | |
terminal.decorate(document.querySelector('#terminal')); | |
terminal.installKeyboard(); | |
}; | |
}); | |
</script> | |
</head> | |
<body> | |
<div id="terminal" style="width:100%; height:100%; position: absolute"></div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment