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.
Keep right on pairing, of course. We just had to figure out how. Our requirements were:
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 SSH.
The configuration we used makes several assumptions about your network’s environment:
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, forwarding
localhost:3000 on the host to
localhost:3000 on the client machine.
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 more homey:
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.
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
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 browser.
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.
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 :)
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!