Electron Adventures: Episode 60: Notebook Perl Engine
Tags: javascript, react, electron, perl
Published: 2021-09-22 14:56:27

We did Ruby, we did Python, time for a classic language you probably aren't seeing much of these days - Perl.

But this isn't just a Perl episode. As doing decent session isolation on Perl side would be quite difficult (and to be honest, even our Ruby/Python versions only did fairly limited isolation), we're flipping how things work:

* previously we had one language server instance, and multiple sessions there
* now we'll create a new language server instance for every session.

### `perl_language_server`

```perl
#!/usr/bin/env perl

use JSON;

sub eval_and_capture {
  my ($code) = @_;

  my $output;
  do {
    local *STDOUT;
    local *STDERR;
    open STDOUT, ">>", \$output;
    open STDERR, ">>", \$output;
    eval($code);
  };
  encode_json({output => $output||"", error => $@});
}

while (<>) {
  my $body = from_json($_);
  my $result = eval_and_capture($body->{code});
  print "$result\n";
  flush STDOUT;
}
```

This was all surprisingly simple.

Perl's `eval` already catches exceptions by deafult, to the very intuitively named `$@` variable, so we don't need to do any kind of `try/catch`. It's actually not a bad default.

If you do `local *STDOUT` in a block, and reopen `STDOUT`, Perl will automatically restore it when it exits the block. This `local` trick works for a lot of things like variables, parts of variables, process `ENV`, and so on, and it's one of the very powerful things in Perl that no other language even tried to copy.

Opening to a reference to a scalar (`\$output`) redirects output to that scalar. It's that `\` character that makes it redirect to `$output` instead of treating it as a file name.

And like in other language servers, we need to `flush` the output, so the buffering doesn't get it our way.

The code doesn't do any session management - everything you do will be in its main scope.

### `src/preload.js`

```javascript
let child_process = require("child_process")
let lineReader = require("promise-readline")
let { contextBridge } = require("electron")

let languageServers = {}

async function startLanguageServer() {
  let process = child_process.spawn(
    "./perl_language_server",
    [],
    {
      stdio: ["pipe", "pipe", "inherit"],
    },
  )
  return {
    process,
    stdin: process.stdin,
    stdout: lineReader(process.stdout),
  }
}

async function runCode(sessionId, code) {
  if (!languageServers[sessionId]) {
    languageServers[sessionId] = await startLanguageServer()
  }
  let { stdin, stdout } = languageServers[sessionId]
  await stdin.write(JSON.stringify({ code }) + "\n")
  let line = await stdout.readLine()
  return JSON.parse(line)
}

contextBridge.exposeInMainWorld(
  "api", { runCode }
)
```

The necessary change is tiny. Instead of single `languageServer` variable, it's now a dictionary of connections, keyed by session id.

We definitely could add some logic for closing processes we no longer use, and error handling, but it's fine for now.

### Result

I wrote the usual Fibonacci code, and then [searched the Internet for the most idiomatic Perl Hello World](https://codegolf.stackexchange.com/a/1876).

Here's the result if we press "Run All" button:

![Episode 60 Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/41u2fkzkwkk578nz8po2.png)

In the next episode we'll start a new project.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-60-notebook-perl-engine).