Electron Adventures: Episode 60: Notebook Perl Engine
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
#!/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
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.
Here's the result if we press "Run All" button:
In the next episode we'll start a new project.
As usual, all the code for the episode is here.