by Tim Caswell
Using FFI to make PTY
Remote Shells
Some years ago I worked at Cloud9 IDE and was tasked with implementing a terminal in the browser. The task is fairly basic and involves a few components.
A terminal emulator running in the browser.
A socket from the browser to the server.
A server that creates pseudo terminals on demand.
The first step is pretty complex, but there are several existing libraries that handle this. I’ve had good luck with term.js. It’s made for node.js and socket.io, but will work anything that feeds it a data stream.
The second step is fairly simple. For this article we’ll be using plain websockets.
The third step is tricky because Luvit doesn’t provide an API for creating pseudo terminals. It’s not part of libuv because implementing it in windows is painful at best, and impossible to do cleanly.
Creating Pseudo Terminals
On most desktop unix systems, there is a system API known as openpty()
. It
looks something like this:
int openpty(int *amaster, int *aslave, char *name,
const struct termios *termp,
const struct winsize *winp);
From the man page we read:
The
openpty()
function finds an available pseudoterminal and returns file descriptors for the master and slave inamaster
andaslave
. Ifname
is notNULL
, the filename of the slave is returned inname
. Iftermp
is notNULL
, the terminal parameters of the slave will be set to the values intermp
. Ifwinp
is notNULL
, the window size of the slave will be set to the values inwinp
.
Once we have these two file descriptors, the built-in spawn and pipe APIs in libuv can take us the rest of the way.
Create file descriptor pair using
openpty()
.Create a
uv_pipe_t
and tell it to open theamaster
fd.Use
uv_spawn()
to create your child process and pass theaslave
fd as it’s stdio to inherit.
It’s all Lua
Thanks to the FFI interface in luajit, we can do this all from lua.
First we need to declare the interface to the system API we will be calling.
local ffi = require('ffi')
-- Define the bits of the system API we need.
ffi.cdef[[
struct winsize {
unsigned short ws_row;
unsigned short ws_col;
unsigned short ws_xpixel; /* unused */
unsigned short ws_ypixel; /* unused */
};
int openpty(int *amaster, int *aslave, char *name,
void *termp, /* unused so change to void to avoid defining struct */
const struct winsize *winp);
]]
-- Load the system library that contains the symbol.
local util = ffi.load("util")
Now we can write a wrapper function to call it.
local function openpty(rows, cols)
-- Lua doesn't have out-args so we create short arrays of numbers.
local amaster = ffi.new("int[1]")
local aslave = ffi.new("int[1]")
local winp = ffi.new("struct winsize")
winp.ws_row = rows
winp.ws_col = cols
util.openpty(amaster, aslave, nil, nil, winp)
-- And later extract the single value that was placed in the array.
return master[0], slave[0]
end
Calling into C from Lua isn’t the prettiest thing, but it beats messing with build systems and non-portable binaries. As long as the API is stable across the systems you care about, FFI is a great choice.
Also since it’s core to the JIT engine itself, using FFI to call C libraries results in a more optimized runtime. Calling into the C binding API is a black box and anything can happen. But calling an FFI function has a clean declared interface that’s easier to optimize.
The Webserver
We’ll be using the weblit framework today to create the HTTP server complete with static file serving and a websocket handler to stream the terminal data to the browser.
-- Initialize the websocket subsystem
require('weblit-websocket')
-- Start declaring our server config
require('weblit-app')
-- It's bound to a local-only port for saftey.
.bind({
host = "127.0.0.1",
port = 9000
})
-- Add some useful middlewares to make it more HTTP compliant.
.use(require('weblit-logger'))
.use(require('weblit-auto-headers'))
.use(require('weblit-etag-cache'))
-- Serve our client app as static files.
.use(require('weblit-static')(module.dir .. "/www"))
-- Handle incoming websocket requests and route to the app module.
.websocket({
path = "/:cols/:rows/:program:",
protocol = "xterm"
}, require('./app'))
-- Start the server.
.start()
This is a pretty simple weblit app that serves a folder as static content
and handles a single websocket route. The cols
, rows
and program
variables
will be passed to the app
handler as request parameters.
The Socket Glue
And finally we implement the glue in the middle that accepts connections from browsers, creates a pseudo terminal, spawns the requested process and pipes data between the browser and the subprocess via the pty pair and the websocket handle.
local uv = require('uv')
local wrapStream = require('coro-channel').wrapStream
local split = require('coro-split')
-- This file is `app.lua` so it's what the server is
-- loading as the connection handler.
return function (req, read, write)
-- Process the parameters from the url pattern.
local cols = tonumber(req.params.cols)
local rows = tonumber(req.params.rows)
local program = "/" .. req.params.program
-- Create the pair of file descriptors
local master, slave = openpty(cols, rows)
-- Spawn the child process that inherits the slave fd as it's stdio.
local child = uv.spawn(program, {
stdio = {slave, slave, slave},
detached = true
}, function (...)
p("child exit", ...)
end)
-- Wrap the master fd in a uv pipe and then in a coro-stream.
local pipe = uv.new_pipe(false)
pipe:open(master)
local cread, cwrite = wrapStream(pipe)
-- Split runs two coroutines in parallel waiting for both to finish.
split(function ()
-- Pipe data from websocket to pty.
for data in read do
if data.opcode == 2 then
cwrite(data.payload)
end
end
cwrite()
end, function ()
-- Pipe data from pty to websocket
for data in cread do
write(data)
end
write()
end)
-- When both are done, clean up.
child:close()
pipe:close()
end
The Browser Side
That’s all there is to the luvit side of this example. The browser is basically a term.js widget with a websocket connection to the luvit server that pipes data between the terminal emulator and the websocket.
A fully runnable example can be found at https://github.com/creationix/lshell.
Vim in a browser, using WebSockets, served by Luvit!
Load Comments...