Skip to content

Instantly share code, notes, and snippets.

@alfredkrohmer
Created August 23, 2016 19:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alfredkrohmer/1a94e3ba3db80820a4cbe19a0ab19b9a to your computer and use it in GitHub Desktop.
Save alfredkrohmer/1a94e3ba3db80820a4cbe19a0ab19b9a to your computer and use it in GitHub Desktop.
Web Terminal for Docker container with Sinatra and Websockets
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