Grimsby is an Erlang Port written in Rust that can close its standard input while retaining standard output (and error)

Overview

Grimsby

ci

An Erlang Port provides the basic mechanism for communication from Erlang with the external world.

From the Ports and Port Drivers: Erlang Reference Manual:

The Erlang process creating a port is said to be the port owner, or the connected process of the port. All communication to and from the port must go through the port owner. If the port owner terminates, so does the port (and the external program, if it is written correctly).

The external program resides in another OS process. By default, it reads from standard input (file descriptor 0) and writes to standard output (file descriptor 1). The external program is to terminate when the port is closed.

An Erlang Port will work exactly as they are designed to, closing the port, terminates the program. A lot of times this is just exactly what you need. Sometimes, however, you need to close standard input (file descriptor 0) and still allow the program to continue running.

For example, the following using just erlang:open_port/2 works because standard input remains open:

1> Port = erlang:open_port({spawn_executable, "/bin/cat"},
                           [binary, eof, use_stdio, exit_status, stream]).

2> erlang:port_command(Port, "hello world!").
true

3> flush().
Shell got {Port, {data, <<"hello world!">>}}
ok

4> erlang:port_close(Port).
true

5> flush().
ok

Whereas sum will not, because requires its standard input to be closed before responding with the checksum:

Port = erlang:open_port({spawn_executable, "/usr/bin/sum"},
                        [binary, eof, use_stdio, exit_status, stream]).

2> erlang:port_command(Port, "hello world!").
true

3> flush().
ok

4> erlang:port_close(Port).
truesum: 
stdout: Broken pipe

5> flush().
ok

The erlang:port_close/1 closes standard input, but also closes standard output (and error). The data message is never received with the checksum as a result.

Grimsby Command

Grimsby is an Erlang Port written in Rust that can close its standard input while retaining standard output (and error).

With grimsby_command, an executable can be spawned, closing stdin if and when necessary, while capturing output on both stdout and stderr:

1> {ok, Spawn} = grimsby_command_sup:start_child(#{executable => "/usr/bin/sum"}).

%% send iodata to the spawned process...
2> ok = grimsby_command:send(Spawn, ["hello", <<" world!">>]).

%% close stdin...
3> ok = grimsby_command:close(Spawn).

%% important to wait for the spawned process to exit...
4> {ok, 0} = grimsby_command:wait_for_exit(Spawn).

%% output is captured from stdout and stderr as iodata:
5> grimsby_command:info(Spawn).
#{exit => 0,
  eof => [stderr, stdin, stdout],
  stderr => [],
  stdout => [[],<<"3785 1\n">>]}

6> grimsby_command:stop(Spawn).

The following parameters can be supplied to the spawned executable with grimsby_command:

%% mandatory full path of the executable to spawn
#{executable := string(),

  %% optional list of arguments (default [])
  args => [string()],

  %% optional map of environment variables
  envs => #{string() => string()},

  %% optional arg0 name
  arg0 => string(),

  %% optional working diretory of the process
  cd => file:filename()

Grimsby Port

Architecture

The module grimsby_port orchestrates the port protocol between Erlang and the Rust process using BERT framed in BURP (32 bit big endian length). It is used by grimsby_command, and can be used by an application that needs asynchronous notification on receiving data, or the exit of the spawned process.

Note that, notifications are sent using gen_statem:send_request/4 and must be replied to! An 'ok' will continue processing, while {error, term()} will stop the process.

The parameters supplied to spawn a process are the same as for grimsby_command, with an additional (optional) parameter of send_to:

%% mandatory full path of the executable to spawn
#{executable := string(),

  %% optional list of arguments (default [])
  args => [string()],

  %% optional map of environment variables
  envs => #{string() => string()},

  %% optional arg0 name
  arg0 => string(),

  %% optional working diretory of the process
  cd => file:filename()

  %% optional pid/name of process to send messages
  %% default to pid of caller:
  send_to => gen_statem:server_ref()}

An example:

1> {ok, Spawn} = grimsby_port:run(#{executable => "/bin/cat"}).
{ok,#Ref<0.1965200363.1370226689.233566>}

2> ok = grimsby_port:send(Spawn, ["hello", <<" world!">>]).
3> ok = grimsby_port:close(Spawn, stdin).
4> flush().

Shell got {'$gen_call',
              {<0.89.0>,[alias|#Ref<0.1965200363.1370292225.236183>]},
              {grimsby_port,{eof,#Ref<0.1965200363.1370226689.233566>,stdin}}}
Shell got {'$gen_call',
              {<0.89.0>,[alias|#Ref<0.1965200363.1370292225.236184>]},
              {grimsby_port,
                  {stdout,#Ref<0.1965200363.1370226689.233566>,
                      <<"hello world!">>}}}
Shell got {'$gen_call',
              {<0.89.0>,[alias|#Ref<0.1965200363.1370292225.236185>]},
              {grimsby_port,
                  {eof,#Ref<0.1965200363.1370226689.233566>,stdout}}}
Shell got {'$gen_call',
              {<0.89.0>,[alias|#Ref<0.1965200363.1370292225.236186>]},
              {grimsby_port,
                  {eof,#Ref<0.1965200363.1370226689.233566>,stderr}}}
ok

Notifications

Assuming the following type definitions:

-type stream() :: stdin | stdout | stderr.
{grimsby_port, {error, Spawn :: reference(), Stream :: stream()}}

This is sent when an error has occured on a stream.

{grimsby_port, {Stream :: stream(), Spawn :: reference(), Data :: binary()}}

This is sent when data is received on stdout or stderr from a stream. Line buffering is present on the Rust streams (which can't currently be disabled).

{grimsby_port, {eof, Spawn :: reference(), Stream :: stream()}}

This is sent when end of file has been reached on a stream.

{grimsby_port, {exit, reference(), integer() | signal}}

This is sent when the processes exits (with a status code) or is killed by a signal.

Protocol

The protocol between the Erlang port and Rust is BERT framed in BURP with a 4 byte unsigned big endian packet length.

The following messages are exchanged over the port:

spawn

{spawn,
 ChildId :: reference(),
 InteractionId :: reference(),
 #{executable := string(),

   %% optional list of arguments (default [])
   args => [string()],

   %% optional map of environment variables
   envs => #{string() => string()},

   %% optional arg0 name
   arg0 => string(),

   %% optional working diretory of the process
   cd => file:filename()}
}

On receipt of this message the Rust port controller will spawn a new process using executable as the full path to the executable, with threads monitoring stdin, stdout and stderr, associating ChildId with the new process.

In response:

{InteractionId :: reference(), ok}

If the process has spawned without error, where InteractionId is the correlating reference for the request.

{InteractionId :: reference(), {error, term()}}

If the process failed to spawn for some reason, where InteractionId is the correlating reference for the request.

eof

{eof, ChildId :: reference(), Stream :: stream()}

This message is received by the Erlang side indicating end of file for stdout or stderr. Where ChildId is the reference used to identify the spawned process in the spawn message.

output

{stdout | stderr, ChildId :: reference(), Output :: binary()}

This message is received by the Erlang side indicating the output from the spawned process either from stdout or stderr. Where ChildId is the reference used to identify the spawned process in the spawn message.

exit

{exit, ChildId :: reference(), integer() | signal}

This message is received by the Erlang side indicating that the spawned process either exited normally with a status code, or has been killed by a signal. Where ChildId is the reference used to identify the spawned process in the spawn message.

error

{error, ChildId :: reference(), Stream :: stream()}

This message is received by the Erlang side indicating an error on a stream of the spawned process. Where ChildId is the reference used to identify the spawned process in the spawn message.

send

{send, ChildId :: reference(), InteractionId :: reference(), Data :: binary()}

On receipt of this message the Rust port controller will send Data to the stdin of the spawned process identified by ChildId from the spawn message.

In response:

{InteractionId :: reference(), ok}

If the data was queued to be sent to the spawned process. An error may be sent asynchronously if the data cannot be written later.

{InteractionId :: reference(), {error, term()}}

If the data could not be queued to the process.

close

{close, ChildId :: reference(), InteractionId :: reference(), Stream :: stream()}

On receipt of this message the Rust port controller will close the stream of the spawned process identified by ChildId from the spawn message.

In response:

{InteractionId :: reference(), ok}

If the request was queued to be sent to the spawned process.

{InteractionId :: reference(), {error, term()}}

If the request could not be queued to the process.

wait for exit

{wait_for_exit, ChildId :: reference(), InteractionId :: reference()}

On receipt of this message the Rust port controller will wait for the exit of the spawned process identified by ChildId from the spawn message.

In response:

{InteractionId :: reference(), ok}

If the request was queued to be sent to the spawned process.

{InteractionId :: reference(), {error, term()}}

If the request could not be queued to the process.

kill

{kill, ChildId :: reference(), InteractionId :: reference()}

On receipt of this message the Rust port controller will kill the spawned process identified by ChildId from the spawn message.

In response:

{InteractionId :: reference(), ok}

If the request was queued to be sent to the spawned process.

{InteractionId :: reference(), {error, term()}}

If the request could not be queued to the process.

License

Apache-2.0.

You might also like...
A webapp that reads your articles to you while you're on the subway
A webapp that reads your articles to you while you're on the subway

ReadToMyShoe Video Demo A website that reads articles to you, even when you're offline. Still in early development. This is a full-stack Rust webapp,

🐱 A high-speed JIT programming language and its runtime, meow~

🐱 A high-speed JIT programming language and its runtime, meow~

A query-building & utility crate for SurrealDB and its SQL querying language that aims to be simple

Surreal simple querybuilder A simple query-builder for the Surreal Query Language, for SurrealDB. Aims at being simple to use and not too verbose firs

Oxido is a dynamic interpreted programming language basing most of its syntax on Rust.

Oxido Table of Contents: Oxido Installation Uninstallation Usage Syntax Data types Variables Reassignments Printing If statements Loop statements Func

Toy language that output pseudocode, pascal and graphviz dot

pseudoc pseudoc is a project I made for school because we needed to write our programs in three different formats: Pascal, pseudocode (similar in stru

A fast, iterative, correct approach to Stackblur, resulting in a very smooth and high-quality output, with no edge bleeding

A fast, iterative, correct approach to Stackblur, resulting in a very smooth and high-quality output, with no edge bleeding. This crate implements a t

In this repository you can find modules with code and comments that explain rust syntax and all about Rust lang.
In this repository you can find modules with code and comments that explain rust syntax and all about Rust lang.

Learn Rust What is this? In this repository you can find modules with code and comments that explain rust syntax and all about Rust lang. This is usef

Parse and encoding of data using the SCTE-35 standard.

SCTE-35 lib and parser for Rust Work in progress! This library provide access to parse and encoding of data using the SCTE-35 standard. This standard

Dinero is a Rust port of Dinero.js (unstable)
Dinero is a Rust port of Dinero.js (unstable)

Dinero is a Rust port of Dinero.js Dinero lets you create, calculate, and format money in Rust. docs.rs/dinero 📦 Install $ cargo add dinero ⚡️ Quick

Releases(0.1.0)
A tool to deserialize data from an input encoding, transform it and serialize it back into an output encoding.

dts A simple tool to deserialize data from an input encoding, transform it and serialize it back into an output encoding. Requires rust >= 1.56.0. Ins

null 11 Dec 14, 2022
Extensible BBCode parser with scoping rules, auto close tags

More BBCode parsers? Yeah! I needed something highly extensible, flexible, and specifically WITH scoping rules so it always produces correct HTML. For

Carlos Sanchez 2 Dec 18, 2022
High concurrency, RealTime, In-memory storage inspired by erlang mnesia

DarkBird is a Document oriented, high concurrency in-memory Storage, also persist data to disk to avoid loss any data The darkbird provides the follow

DanyalMh 25 Dec 15, 2022
🚀simple server that returns error codes with their respective messages and debug information, written in rust 🦀

ErrorServer ?? A simple & lightweight server that returns a HTML page of the error code with its respective message and debug information, written in

Jakob 2 Dec 15, 2022
Rust 核心库和标准库的源码级中文翻译,可作为 IDE 工具的智能提示 (Rust core library and standard library translation. can be used as IntelliSense for IDE tools)

Rust 标准库中文版 这是翻译 Rust 库 的地方, 相关源代码来自于 https://github.com/rust-lang/rust。 如果您不会说英语,那么拥有使用中文的文档至关重要,即使您会说英语,使用母语也仍然能让您感到愉快。Rust 标准库是高质量的,不管是新手还是老手,都可以从中

wtklbm 493 Jan 4, 2023
A typesafe, flexible, simple, and user-friendly unit system library for Rust that has good error messages.

uy A typesafe, flexible, simple, and user-friendly unit system library for Rust that has good error messages. Usage uy not only stores the unit of a v

Lachlan Sneff 19 Aug 8, 2023
🚃 lib for CLI utilities, printing, and error handling

axocli Common code for setting up a CLI App and handling errors/printing. Example See examples/axoapp.rs for a walkthrough/example. Some various inter

axo 5 Apr 4, 2023
📱️🚫️🌝️💾️ 3FakeIM is a joke program meant to imitate various fictional characters, and the "[CHARACTER] CALLED ME AT 3:00 AM" clickbait trend, while poking fun.

3FakeIM ??️??️??️??️ 3FakeIM is a joke program meant to imitate various fictional characters, and the "[CHARACTER] CALLED ME AT 3:00 AM" clickbait tre

Sean P. Myrick V19.1.7.2 2 Jul 3, 2023
This blog provides detailed status updates and useful information about Theseus OS and its development

The Theseus OS Blog This blog provides detailed status updates and useful information about Theseus OS and its development. Attribution This blog was

Theseus OS 1 Apr 14, 2022
Functions for mapping plaintexts to a u64 while preserving sort order

ore_encoding.rs This is a companion package to ore.rs that can generate and manipulate u64 plaintexts before they are encrypted by ore.rs. Being able

CipherStash 2 Dec 14, 2022