Skip to content

Processes

A process is an instance of a program on a node. Programs are compiled into a node; processes are the live thing doing work. A process exists from the moment it is created: it has a pid, it appears in ps, and it answers to kill. It is either running or suspended - created suspended, and running it lifts the suspension.

ps lists the processes inside the node - Dusk's own, like docker ps, not the host operating system's - kill signals one by pid, and waitpid waits for one to exit.

What you implement

A program's runtime is a type implementing ProcessMixin (from dusk_program), declared #[async_trait(?Send)] - processes are executor-local, like the namespace they run in. You implement three methods:

  • with_context(ctx) - build the process from its ProcessContext (its pid, the node's namespace, and the caller's program args). It is async, so construction can do setup work.
  • main(signal_receiver, ready) - the process body: do the work, announce readiness, then handle signals.
  • portal() - return the process's typed portal.

The identity methods (pid, program_id, name, version) are generated by #[derive(Process)] from a #[process_context] field - you don't write them.

Signals

main is given a signal_receiver (a DynamicReceiver<Signal>). A client's Dusk.kill(pid, signal) is delivered here: wire value 15 arrives as Signal::Terminate (the request to exit); any other value arrives as Signal::Unknown(n). Three are exceptions: wire values 7 (Sweep) and 8 (Reap) are handled by the node itself and never delivered to a process, and Signal::Rerun(args) does not come from kill at all - it arrives when someone builds a process at a pid this one already holds. The common shape is a loop on signal_receiver.receive().await that returns from main on Terminate. A process that also does ongoing work selects that work against the receiver.

Readiness

main is also given a ready handle. A process announces it can accept portal calls by sending true on it. A client's process.portal() resolves only once readiness fires, so a client can call into a process the moment it is reachable without racing its startup.

Daemon vs in-session

Dusk.process creates a process and registers it in the namespace, suspended. The client then chooses the process's lifetime:

  • Dusk.run(process) spawns the process as its own task on the node, so it outlives the client session that created it. This is how long-lived processes (and the node's own init) run.
  • process.run() runs the process inside the calling session, so it is torn down when that session ends.

Running a process that is already running does nothing. A process that has exited - or was terminated while suspended, which has nothing running to deliver a signal to - stays in the namespace with its exit status until something waitpids it, the way a Unix zombie does. waitpid is what takes a process out; so does the Reap signal, for a caller that wants the process gone without reading its exit status. A process that was created and never run is not a zombie and has no exit status to read, so Sweep is what takes that one out. The runtime machinery behind this is internal; see Architecture. A process that wants to keep running after a one-shot shell command does so through a shell convention, not a core process mechanism.