While it's always best to pair program side-by-side with your pair, it's not always feasible. Over the last couple of weeks, Jim Remsik and I needed to pair on a new project, but circumstances dictated that he be elsewhere.
What's a Pair to Do?
Keep right on pairing, of course. We just had to figure out how. Our requirements were:
- Collaborative Editing: the fundament of pairing, both people need to be able to type into the same editor.
- Access to the Local Server: it's pretty hard to develop an app you can't see.
- Ease of Communication: we need to be able to hear each other, preferably seeing each other as well (there's a lot of nonverbal communication when pairing).
- Light Weight: the internet connections would be of variable quality, so we required something that wouldn't tax small pipes.
We'd previously used iChat Screen Sharing when we needed to do this at Hashrocket… it works great for numbers 1 and 2, gets us half way to number 3, but falls over entirely on number 4. The lag from sending so much data over the network starts out as merely grating, but quickly crescendos in abject frustration, tending to make the client of the pairing session little more than an observer.
Given that, the no-brainer for us was to use Vim inside a terminal-multiplexer that allowed multiple client connections, so we could attach to the session over SSH. That'd get us "Collaborative Editing", but what about the other requirements? After a bit of discussion, we decided we could solve numbers 2 and 4 by forwarding port 3000 of the host machine to the client attaching via SSH. Being that this was such a lightweight solution, we opted for Skype (our communications weapon of choice) for video chat.
Our initial permutation involved opening a port in our SonicWALL firewall and
manually forwarding the port to a machine inside the firewall with a static
IP. This worked great, after the initial pain of configuring SonicWALL (which
is, in my estimation, a FPOS). The shortcoming here is that we didn't want to
have to open up a port for every station we wanted to remote-pair from (some
days Hashrocket is an elaborate game of musical chairs). tpope came
to the rescue with his recommendation for using the
ProxyCommand option for
Pairing Down the Configuration
The configuration we used makes several assumptions about your network's environment:
- At least one machine in your network is available at a public IP
- The externally available machine understands MDNS (e.g. Bonjour)
- The other machines on your network are available via MDNS
That said, it should be relatively easy to modify this to work with a direct peer-to-peer connection (provided the host has a routable IP address). If you're so inclined, please contribute in the comments below.
You'll need an entry for the machine you're tunneling into, ours looks like this:
Host tunnels Hostname 0.0.0.0 # this should be an externally routable address User your_user
Now here's the magic:
Host *.hashrocket #* User your_user ProxyCommand ssh -ax tunnels nc `echo %h|sed -e 's/\.hashrocket$/.local/'` %p 2>/dev/null
Let me attempt to unravel what this is doing.
ProxyCommand allows you to
specify a command to use to connect to the host machine. In our case, we're
doing a wildcard match on anything at
.hashrocket, the actual match is then
used to figure out how to route the request inside the network. So in our
case, tunneling to
fry.hashrocket gets us to our tunnels machine, then we
use netcat to echo the given command to
Which brings us to the actual forwarding command:
$ ssh fry.hashrocket -L 3000:localhost:3000
This drops us into a shell on
fry.local and establishes the reverse tunnel,
localhost:3000 on the host to
localhost:3000 on the client
If you're interested in why we chose
screen, I'll first refer you to the
tmux FAQ and
subsequently admit that I went with it primarily because it's newer and
shinier. You could easily substitute
tmux here, though my opinion
after a few weeks use is that it's much more intuitive and I won't be making
the move back.
The relevant commands for creating and attaching to a new session follow. On the host, you'll need to create a session:
$ tmux new -s mysession
On the client, you'll need to attach to that session:
$ tmux attach -t mysession
Once you're connected, you'll need to run Vim (or some other terminal-friendly editor, e.g. emacs, sorry TextMate people) in a window or pane. We opted to run Terminal full screen on an iMac, with two panes arranged vertically; a shell session in one, Vim in the other and new windows for long-running processes or utility shells.
We're Vimtarded, so putting the following in
~/.tmux.conf made us feel a bit
setw -g mode-keys vi bind h select-pane -L bind j select-pane -D bind k select-pane -U bind l select-pane -R
The initial configuration puts navigation commands in Vim mode (you'll see this in copy mode, the help screen, etc). The custom bindings below that make navigating the panes within a window work like navigating splits in Vim.
tmux crash course
The default binding for the command mode is
ctrl-b. We will refer to it as
ctrl-b, then 'd'.
<command>-d # detach from current session <command>-" # split window into panes horizontally <command>-% # split window into panes vertically <command>-o # go to next pane <command>-x # close current pane <command>-? # display available keybindings <command>-c # create a new window <command>-n # go to next window <command>-p # go to next window
Pairing is Such Suite Sorrow
So now we're editing text together, we're looking at
localhost on each end,
we're running some specs, writing some cucumbers, everything's perfect. Until
you need to
save_and_open_page. You have two options: never make any errors
or move those files somewhere that your pair can see them too.
Since we've yet to be issued our Ninja-Passes, we opted for the latter and since we're using Capybara, we did it like so:
Capybara.save_and_open_page_path = "public/tmp"
Awesome, now they can just browse to
http://localhost:3000/tmp and see
Capybara temp pages! That is, if they can guess the name of them, or want to
ls the directory, copy the name of the last file and paste it into the
That would work, but how about this instead:
At the bottom of
if Rails.env.development? Some::Application.routes.draw do match 'tmp/(:action)', :controller => 'tmp' end end
class TmpController < ApplicationController expose(:temp_pages) do Dir.glob(File.join('public','tmp','capybara-*')) end def latest render temp_pages.last, :layout => false end end
Adding an index view would be trivial if you needed to look at more than just the latest one1.
The Pair Stare
As a side-note, we did use Skype to video chat as our primary means of
communication. Jim claims that the best part of it was that I Skyped from my
machine (a 13" MBP, with my iPhone headset) and hosted the
tmux session on
the iMac right beside it. This had the unintended side effect of me not
"staring at him" all day long while I coded. It's the little things that make
all the difference :)
Check Out The Pair On This Guy
While my brief stint of remote pairing has come to a close, I've no doubt I'll need to do it again at some point in the future and am interested in refining this process. So please, if you can think of a way to improve on or simplify any of this, leave a note in the comments!