Rama: CoffeeScript TCP Terminal Proxy

Heroku has a very cool, useful feature where you can run a one-time process in its own isolated container ("dyno"), which is great for doing database migrations or other administrative tasks. It also gives users the ability to "attach" to these containers to get data back or interact with the remote shell, with a terminal proxy they call Rendezvous, which basically just streams TCP back & forth over SSL

When we first looked at this we thought it would be a major headache to implement for Pogoapp, but after a lot of searching and a bit of sad, semi-working EventMachine code, we came across the node.js pseudo-terminal bindings pty.js, which combined with a bit of CoffeeScript made for a very simple TCP terminal proxy we've named rama (*).

We have some custom internal logic for routing the TCP connections into the correct ephemeral container, but once the container is booted and your socket gets proxied into it, the entire terminal proxy is just these ~50 lines of CoffeeScript:

net       = require 'net'
fs        = require 'fs'
pty       = require 'pty.js'

rama_cmd           = process.env.RAMA_CMD || "bash"
port               = process.env.PORT || 8000

anyone_connected   = false
last_term_data     = null
last_socket_data   = null
open_timeout_limit = 20000
read_timeout_limit = 60000

server = net.createServer (socket) ->
  anyone_connected = true
  last_term_data   = new Date

  term = pty.fork 'bash', ['-l', '-c', rama_cmd], {
    cols: parseInt(process.env.TERM_COLS || 80),
    rows: parseInt(process.env.TERM_ROWS || 30),
    cwd: '/app', 

  socket.on 'data', (data) ->
    last_socket_data = new Date

  term.on 'data', (data) ->
    last_term_data = new Date

  term.on 'close', ->
    console.log 'terminal closed, exiting process'

server.listen port, ->
  console.log "server bound to #{port}"

# initial connection timeout
stop_unless_connected = ->
  unless anyone_connected
    console.log "no one connected: exiting"

# idle connection timeout
exit_if_idle = ->
  now = new Date
  last_term_ago   = now - last_term_data
  last_socket_ago = now - last_socket_data

  if anyone_connected && last_term_ago > read_timeout_limit && last_socket_ago > read_timeout_limit
    console.log "timeout in #{read_timeout_limit} ms (term: #{last_term_ago} | socket: #{last_socket_ago}), exiting process"
    setTimeout exit_if_idle, (read_timeout_limit / 6)

setTimeout stop_unless_connected, open_timeout_limit

We use the TERM_COLS, TERM_ROWS, and RAMA_CMD environment variables to pass in the size of the user's terminal and the command they want to run. The actual proxy-code is dead-simple, and we record timestamps when the connection starts and when data is transferred so the app can exit itself when it's not being used

When you want to run a remote command on pogoapp, like pogo run bundle exec rails c to get a Rails console, we just boot a container for your app the way we normally would, but add the compiled slug for rama to the vendor directory of your app, and then boot up rama with RAMA_CMD set to "bundle exec rails c"

You can run rama yourself with:

npm install
coffee rama.coffee

and give it a basic test by connecting with nc or telnet