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.
Top comments (0)