thox contexts and interactions¶
Inter-process communication (IPC) serves, from a process’s point of view, for communicating with the hardware and other processes, allowing them to cooperate to accomplish a task. It is accomplished in thox using two simple mechanisms:
Remote procedure call (RPC) calls.
Message queues (not implemented yet).
These mechanisms take place in contexts. Contexts are security objects in which these two interactions take place; they allow processes to manage these elements, amongst others:
Sandboxing of given processes (by whitelisting or blacklisting RPC endpoints they can call).
Logging (by running the process from a process “watcher”).
Compatibility with older thox processes (by converting older RPC endpoints into new ones).
They can also correspond to objects, such as file handles or network connections, where the handle is guaranteed to the daemon to be closed since references to given contexts are closed automatically when a process exits.
Regarding contexts, a process:
Has a default context, named context 0.
Can get access to other contexts when receiving an answer to an RPC call, using
os.pull()
, or creating one, usingos.context()
.Can share access to a context by answering a call using
os.answer()
, or creating a new process usingos.run()
.
A context is owned by the process which has created it using
os.context()
. This process manages the context, that means it
manages the routing of the RPC calls and messages, thus the security of it
in case it shares this context with multiple processes.
The initial process gets a context managed by the process manager; the initial process can then create contexts for its children, depending on the security it wants to setup. See initd: the thox init daemon for more information.
Todo
Message queues in a different namespace, also in contexts, with the following functions, probably:
os.push(ctx, name, info)
os.listen(ctx, name)
This requires similar utilities to RPC calls, as we also need routing (although while calls must arrive to one desintation, messages can arrive to multiple).
Todo
What happens on OS shutdown for example, in which order are processes closed if they need to close processes? Can processes have a “kill” event in order to be able to make their last actions (e.g. sending disconnect messages on network connections)?
Events¶
thox processes are event-driven; the process manager prepares the events for the process to read, with an optional filter depending on what it expects.
Events can be the following:
RPC events, such as calls received by the process and answers received from previous calls.
Messages from messages queues the process has subscribed to. These messages can represent “real world” events, etc.
Events are represented using os.Event
objects, and are pulled
using os.pull()
. It is possible to pull specific events, such as
the answer to a specific call, by specifying additional parameters to
os.pull()
; by default, it returns the oldest event not gathered
by the process.
Remote Procedure Call (RPC)¶
thos processes communicate mainly in a one-to-one fashion using a remote procedure call protocol.
Picture three processes P1, P2 and P3, where P3 manages the default context of both processes P1 and P2. P1 wants to execute an action using this protocool, and P2 has this function available and wants any other processes using its default context to be able to run it.
In order to represent the action, thox uses RPC names such as
my.super.function
. When started up, P3 first decides, either for each
action or globally, what it wants to do. Some common possibilities are:
It provides a fix set of functions, and does not provide any mechanisms to “bind” functions.
It transmits all calls from a given context to another, e.g. calls from a context it created to its default context.
It allows RPC name binding.
The basic context most processes on thox will encounter is the context
provided by initd (see initd: the thox init daemon for more information).
This context allows binding through its os.rpc.bind()
and
os.rpc.unbind()
endpoints.
With binding, daemons such as P2 can then route specific RPC calls done on
its default context to itself, which means that subsequent calls by any
process to my.super.function
on the context provided by P3 will result
in P2 receiving a call from the said process. Therefore, when making a
call to my.super.function
to its default context, P1 will receive an
answer from P2.
What happens in order during the call is the following:
P1 calls the procedure using the
rpc
call. This actually emits a call to the system usingos.call()
, which returns a token in the form of a numerical Call IDentifier (CID).P3 gets a call event, bundled with the CID with which to answer, the arguments given by P1, and some additional request information. It finds out that P2 is bound to the given name, and transmits the call to it using
os.transmit()
.P2 gets a call event, bundled with the CID with which to answer, the arguments given by P1, and some additional request information.
P2 treats the request accordingly.
P2 emits an answer using
os.answer()
, passing the CID to it, optionally followed by some return values.P1 gets an answer event, with the CID (to distinguish the call to which the answer is for, in case P1 has sent multiple calls).
For binding a name beforehand, P2 uses os.rpc.bind()
; it can also
unbind a name using os.rpc.unbind()
.
RPC names¶
Names are actually a dot-joined collection of Lua Names
:
Names (also called identifiers) in Lua can be any string of letters, digits, and underscores, not beginning with a digit and not being a reserved word. Identifiers are used to name variables, table fields, and labels.
Note that these names have no arbitrary maximum length; indeed, the
Lua implementation used in both ComputerCraft and OpenComputers, Cobalt,
does not implement one for names.
However, to avoid confusions, RPC names are case-insensitive, which means
that fs.getspaceleft
and FS.GetSpaceLeft
lead to the same endpoint.
Some valid and invalid identifiers are the following:
Valid identifiers |
Invalid identifiers |
---|---|
sleep os.module how.deep.does.this.go my.function2 |
for 123hello hello.2theworld my.gawd$ |
The rationale behind this definition is to be able to integrate these
identifiers into native code using the rpc
prefix, for example
rpc.sleep(5)
to emit a synchronous call to the sleep()
function.
Case insentivity is explained by the confusion that the system-wide
difference between fs.GetSpaceLeft
and fs.getspaceleft
could generate,
leading to potential security problems; see typosquatting for a real world
problem alike what this mitigation is addressing.
Notice that while this API is asynchronous, most simple programs will only
need to call RPC functions synchronously; to simplify this, the user can
use the os.rpc
object.