Running VS Code server over SSH using SLURM (without overburdening the login node)

VS Code has an extension, Remote SSH, which makes it possible to develop remotely by establishing and connecting to a remote VS Code server over SSH. You can edit files, open terminal shells, even run jupyter notebooks all in VS Code. It’s really nice.

The tricky part: clusters have login nodes which are the only way to ssh in, but are resource constrained. To avoid using up too many resources on the login node, you shouldn’t be running your VS Code server on it. Also, if you want to run a jupyter notebook in VSCode, then you really need to do it on a compute node.

The typical solution is to get an interactive job for a compute node, and run code in there. But how can you run your VS Code server (and all the accompanying terminals, jupyter notebooks, used when using VS Code) on a compute node? SSH-ing directly into the compute node isn’t possible. Instead, you have to set up a proxy through the login node.

Here’s how to connect VS Code server over SSH to a compute node using SLURM:

Step 1: connect to login node via SSH:

ssh sca63@g2-login-05.coecis.cornell.edu

Step 2: get an interactive job that will run your code session:

srun --nodes=1 --gres=gpu:1 --cpus-per-task=8 --time=04:00:00 --mem=50G --partition=default_partition-interactive --pty bash

    Step 3: add the compute node’s name to your list of hosts in your ssh config file (~/.ssh/config) with something like this:

    Host <compute-node-name>
      User <ssh username>
      ProxyCommand ssh <ssh username@<ssh login address> -W <compute-node-name>:%p

      For example, it might look like:

      Host ellis-compute-01
        User sca63
        ProxyCommand ssh sca63@g2-login-05.coecis.cornell.edu -W ellis-compute-01:%p

      Step 4: In VS Code, open the command palette, select Remote-SSH: Connect to Host..., choose your compute node name. Then you should be good to go!

      Addendum: Making things easier

        If you’re like me, you’re on a cluster with a bunch of nodes, and you get a new node each time you get an interactive job. 

        Solution 1: make a shortcut for adding a new node to the file. 

        addnode() {
            NODE_NAME=$1
            CONFIG_FILE="$HOME/.ssh/config"
        
            # Create config entry
            CONFIG_ENTRY="Host $NODE_NAME
        ProxyCommand ssh sca63@g2-login-05.coecis.cornell.edu -W $NODE_NAME:%p
        User sca63"
        
            # Add entry to config file
            echo "$CONFIG_ENTRY" >> "$CONFIG_FILE"
            echo "SSH config entry added for $NODE_NAME"
        }
        

         Solution 2: get all the node names and add entries for all of them in advance

        First get the names of all the nodes from the cluster with sinfo -N -h -o '%N

        Then add the config lines to the SSH config file:

        # convert to sorted list of unique names
        names = sorted(list(set(names.split())))
        
        # get the config lines for each name
        def lines(name):
            s = """Host {name}
          ProxyCommand ssh sca63@g2-login-05.coecis.cornell.edu -W {name}:%p
          User sca63
        
        """
            return s.format(name=name)
        
        s = ''.join([lines(name) for name in names])
        
        with open('/Users/simon/.ssh/config', 'a') as f:
            f.write(s)

        Converting image to .EPS on Mac

        I needed to convert a folder of around 25 .png files to .eps.

        TLDR: use imagemagick.

        You can install with brew: brew install imagemagick

        This installs a command convert.

        for a single file: convert image.png image.eps works!

        For multiple files, put them in a folder, make that folder your current directory, and run

        for FILE in $(ls)
        do
        convert $FILE {$FILE%png}eps
        done

        Really this works even if have Windows or Linux, but I’m putting Mac in the title because I got very frustrated with how long it took me to discover this solution.

        Undoably Secure Screws

        The screws on the back of my laptop repeatedly loosen themselves. How can I tighten them in place, while still being able to loosen them again at some point?

        TLDR: use a glue stick. Worked for me

        First the screws would loosen every few months. Then every few weeks, then every week, and now it’s happening every few days.

        This is one of those “un-Googleable” problems—problems which are hard to find solutions for, not because solutions aren’t out there, but because Google doesn’t understand the query and prioritize results. “tighten screws in place but can still be loosened again”? “prevent screws from loosening but still be able to unscrew”? “laptop screws keep loosening”? Couldn’t find any results.

        I figured I should start with the weakest glue possible, and slowly increase strength until this ceases to be an issue. So, we’re starting with a glue stick. I smashed each little screw into the glue stick then tightened back onto my laptop case. I checked, and you can still loosen the screws if needed. We’ll see if they self-loosen. I’ll update this post based on the long-term results.

        Update: six months post gluing, my laptop broke and I had to get a new one. Never had any issue with the screws self-loosening.

        Compiling Latex/Bibtex from Vim… without plugins

        You’re probably better off using a plug-in. I did it manually, and I forget why. But here it is. Pretty simple once it’s all said and done, but it took me ages to figure out the bash/zsh code to make it work.

        Step 1. Add the following functions to your bash/zsh/etc. env:

        latte () {
        s=${1%tex}pdf
        pdflatex $1
        open -a "Preview" $s
        }

        latte2 () {
        s=${1%tex}pdf
        t=${1%tex}aux
        pdflatex $1
        bibtex $t
        pdflatex $1
        pdflatex $1
        open -a "Preview" $s
        }

        Step 2. Add the following to your vimrc:autocmd FileType tex,latex noremap gl :w<CR>:!latte %
        autocmd FileType tex,latex noremap gb :w<CR>:!latte2 %

        Step 3. When you wish to compile your latex file, type gl. If you wish to compile your bibtex too, type gb.

        Python Debugging in Vim & VS Code Made Easy

        It’s as simple as using this mapping: noremap <LEADER>p oprint(f"<ESC>pa: {<ESC>pa}")<ESC>

        Then, whenever I want to debug something, I yank the name of the variable (usually via ye), then type <LEADER>p, which makes a print statement for me:

        Note: that’s an old animation when before I changed the mapping to make use of Python 3’s f-Strings.

        My leader is the spacebar. I use https://github.com/machakann/vim-highlightedyank to highlight my yanks.

        Edit January 2022: I have moved to VS Code. To achieve the same with the Vim plugin, I have the following added to my vim.normalKeyBindings:

               { "before": ["<LEADER>", "p"], "after": ["o", "p", "r", "i", "n", "t", "(", "f", "'", ":", " ", "{", "}", "<ESC>", "P", "^", "f", "'", "p", "^"]},
        

        Coping from Vim to System Clipboard over SSH

        Copying to the clipboard in Vim is easy: "*y.

        What if you want to copy to your local keyboard from a vim session over ssh? "*y no longer works here.

        Highlighting text in iTerm could theoretically work. I’m using tmux with Vim over ssh, and highlighting text really doesn’t work, for whatever reason. Until now, I was stuck retyping everything, or settling for a screenshot.

        But I found a nice solution with the help of this stackoverflow post: https://stackoverflow.com/questions/1152362/how-to-send-data-to-local-clipboard-from-a-remote-ssh-session

        There are two components:

        • a vim mapping which yanks to a “personal vim clipboard” at ~/.vim/clip.txt
        • a command run locally which ssh’s in and grabs the text from the vim clipboard,

        The vim mapping:

        noremap <LEADER>a :e ~/.vim/clip.txt<CR>:%d<CR>"0P:w<CR>:bd<CR>:echo "copied clipboard to ~/.vim/clip.txt"<CR>

        This puts whatever you have currently yanked in the default register into the clipboard file. I imagine there is a way to configure yanking to “this clipboard” the way you would any other clipboard (doing "cy or something), but this works fine for me.

        The ssh command is:

        alias clip='ssh -Y salford@my_remote "cat ~/.vim/clip.txt" | pbcopy'

        which I put in my bash/zsh environment so that I can get the clipboard text easily. I really only use one remote server often, so I’m content having an alias for that address specifically.

        The vim mapping does the following in order:

        • :e ~/.vim/clip.txt<CR> (open the clipboard in a buffer)
        • :%d<CR> (delete the old clipboard text)
        • "0P (paste the yanked text into the clipboard—we do “0 since the last command filled up the default register)
        • :w<CR>:bd<CR> (write and close the clipboard buffer)
        • :echo "copied clipboard to ~/.vim/clip.txt"<CR> (give some feedback that this happened)

        The ssh command ssh’s into the remote server, cat’s the clipboard, and pipes the output into pbcopy, which copies the input into the system keyboard.

        Maybe this will be helpful to someone. I was pleased with the vim mapping.

        Configuring Vim bindings in your Jupyter Notebooks

        Earlier this month I found jupyter-vim-binding, a Jupyter Notebook extension which “enables a Vim-like environment powered by Code Mirror’s Vim“. This extension provides some pretty fun capabilities. After installing, though, I noticed some limitations:

        • Key repeats didn’t work. Whenever I held down j or k to move lines, I moved one line then stop. Holding some keys, like l, would make a box with an accent mark show up.
        • Using ShiftEscape to exit Vim command mode into Jupyter command mode didn’t work well. I use Karabiner-Elements to let my Caps Lock key be mapped to press as Escape when pressed alone and Control otherwise. This means when I try to type Shift-Escape it would register as Shift-Control, unless I let go of Escape before Shift, which is not natural.
        • None of my mappings/configurations I usually use in Vim from my vimrc were there. Notably
          • Relative line numbers were not shown
          • s and S were not mapped to ^ and $
          • No line wrapping

        The above listed are those issues which I managed to fix in this extension—scraping by with my minimal JavaScript knowledge! I thought I would share how.

        Key Repeats in jupyter-vim-bindings

        My issue with key repeats was not the issue of the extension but rather my Mac, which didn’t enable key repeats at all in Chrome and other applications. I followed this website’s advice, entering

        $ defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool false
        

        into the terminal and restarting Chrome.

        Editing and reloading the jupyter-vim-bindings extension

        Now I’d like to make Shift-Escape work, or add another key binding to exit Vim command mode. To modify the jupyter-vim-binding extension, cd into the vim-binding directory of the extension and edit vim-binding.js. I’ll show the edits I made below. After you’re done editing, you can reload the extension with the following commands:

        jupyter nbextension disable vim_binding/vim_binding
        jupyter nbextension enable vim_binding/vim_binding
        

        Then run a new Jupyter notebook, and the extension should be updated.

        Defining new mappings in jupyter-vim-bindings

        Now let’s get back to editing. At line 70 of vim_bindings.js, the variable cm_config.extraKeys is set. You can see where Esc and Shift-Esc are mapped to leave Insert and Normal mode. Adding a new mapping is as easy as adding a new line such as (what worked in my case)

          'Shift-Ctrl': CodeMirror.prototype.leaveNormalMode,
        

        You could also remap a key not used in this extension, like Z:

          'Z': CodeMirror.prototype.leaveNormalMode,
        

        This lets us remap s and S too:

              'S': function test(cm) {
                  cm.execCommand("goLineStartSmart");
              },
              'Shift-S': function test(cm) {
                  cm.execCommand("goLineEnd");
              },
        

        To get this I used the CodeMirror documentation. What’s really going on here is that CodeMirror lets us define extra key mappings with the extraKeys configuration mapping. Each entry in the list maps a key string to a function which takes the editor instance cm (also called editor by many other sites) and then uses the CodeMirror programming API to do something.

        When we previously mapped Shift-Escape to CodeMirror.prototype.leaveNormalMode, we really mapped to this function found at line 197 of lib/codemirror.js:

            CodeMirror.prototype.leaveNormalMode = function leaveNormalMode(cm) {
              ns.notebook.command_mode();
              ns.notebook.focus_cell();
            };
        
        

        So now for s and S we create a function which calls cm.execCommand. Details of how this works can be found in the CodeMirror documentation.

        Showing Relative Numbers in jupyter-vim-bindings

        The CodeMirror documentation tells us that lineNumbers is a Configuration option. Configuration options get set when we create a CodeMirror editor instance via CodeMirror and fromTextArea functions, and can be changed via the setOption method. But at first glance such code is hard to find in jupyter-vim-bindings. The correct place is line 79 of vim_bindings.js, after which we call cm.setOption several times to set options like keyMap. To add line numbers, we can add our own:

        cm.setOption('lineNumbers', true)
        

        Getting line wrapping is as easy as:

        cm.setOption('lineWrapping', true)
        
        

        What about relative numbers? This is a bit more tricky. I found the answer here. Just as we added a cm.setOption line above, we can add the following:

        cm.on('cursorActivity', function(cm) {
          const lineNum = cm.getCursor().line + 1;
          if (cm.state.curLineNum === lineNum) {
            return;
          }
          cm.state.curLineNum = lineNum;
          cm.setOption('lineNumberFormatter', l =>
            l === lineNum ? lineNum.toString().concat('-') : Math.abs(lineNum - l));
          });
        

        I edited the original solution a bit so that the current line would have a dash next to it, as I felt otherwise the numbers looked a bit confusing. The final code block looks like this for me:

            ns.notebook.get_cells().map(function(cell) {
              var cm = cell.code_mirror;
              if (cm) {
                cm.setOption('lineNumbers', true)
                cm.setOption('lineWrapping', true)
                cm.on('cursorActivity', function(cm) {
                  const lineNum = cm.getCursor().line + 1;
                  if (cm.state.curLineNum === lineNum) {
                    return;
                  }
                  cm.state.curLineNum = lineNum;
                  cm.setOption('lineNumberFormatter', l =>
                    l === lineNum ? lineNum.toString().concat('-') : Math.abs(lineNum - l));
                });
                cm.setOption('keyMap', cm_config.keyMap);
                cm.setOption('extraKeys', $.extend(
                  cm.getOption('extraKeys') || {},
                  cm_config.extraKeys
                ));
              }
            });
        

        Other features?

        There were several tweaks I haven’t figured out how to do yet which would be nice to have:

        • let s and S work with other commands, such as ds.
        • Remap ; and :
        • Show a vertical line at 80 characters. Suggestions are here but I couldn’t get them to work.
        • Map Y to y$
        • many more small but unimportant things from my vimrc

        Let me know if you or anybody has found solutions to these!