Skip to main content

Module daemon

Module daemon 

Source
Expand description

Daemon plugins: run inside standardd on the machines the plugin is enabled on, never render, and may reach the machine through granted system interfaces (process, watch, panes; with the wasi feature, files, sockets and HTTP).

A daemon plugin has an async DaemonPlugin::run loop driven by a small single-threaded executor: the host calls the component’s drive export when the loop’s next timer is due and after every event, and the loop awaits Context::next_event, Context::sleep and, with wasi, WASI pollables (daemon::io). What happens on the machine arrives as events too: Event::FileChanged for a watch, Event::ProcessOutput and Event::ProcessExited for a process, Event::PaneChanged after panes::subscribe. A companion is a daemon plugin that answers its UI plugin’s calls in DaemonPlugin::call; Context::caller says who asked.

Plugin methods take &self: the run loop and call handlers share the plugin, so keep mutable state in a RefCell or Cell.

ⓘ
//! The companion daemon of `together_ui.rs`: the same plugin id, kind
//! `companion`. It runs once (the singleton daemon), does the work, stores
//! the result as a durable value and announces it with an event; the UI
//! halves in every viewer read the value.
//!
//! Manifest:
//!
//! ```json
//! {
//!   "apiVersion": 2, "kind": "companion", "id": "together", "version": "0.1.0",
//!   "module": "together_companion.wasm",
//!   "grants": { "process.exec:make": "Runs make in your project when you ask for a build" }
//! }
//! ```
#![no_std]

extern crate alloc;

use alloc::rc::Rc;
use core::cell::Cell;

use serde::Serialize;
use standard_plugin::daemon::{Context, DaemonPlugin, process};
use standard_plugin::prelude::{Error, Json, Target, format};

#[derive(Serialize)]
struct Status<'a> {
    state: &'a str,
    builds: u32,
}

/// Stores the status where every viewer's UI half reads it, and tells
/// them it changed.
fn publish(cx: &Context, state: &str, builds: u32) {
    let _ = cx
        .values()
        .set("together.status", &Status { state, builds });
    let _ = cx.events().emit("together.changed", &(), Target::Viewers);
}

/// One build, run as a task on the daemon's executor.
async fn rebuild(cx: Context, builds: Rc<Cell<u32>>, running: Rc<Cell<bool>>) {
    publish(&cx, "building", builds.get());
    let passed = process::spawn("make", &[], None)
        .and_then(|child| child.wait())
        .is_ok_and(|status| status == 0);
    builds.set(builds.get() + 1);
    running.set(false);
    publish(&cx, if passed { "passed" } else { "failed" }, builds.get());
}

#[standard_plugin::daemon]
struct Companion {
    builds: Rc<Cell<u32>>,
    running: Rc<Cell<bool>>,
}

impl DaemonPlugin for Companion {
    fn activate(_cx: &Context) -> Self {
        Self {
            builds: Rc::new(Cell::new(0)),
            running: Rc::new(Cell::new(false)),
        }
    }

    async fn run(&self, cx: Context) {
        publish(&cx, "idle", self.builds.get());
    }

    /// Calls arrive from the UI half. A handler answers at once; longer
    /// work goes to a task, which runs when the host next drives the
    /// plugin (right after the call).
    fn call(&self, method: &str, _request: Json, cx: &Context) -> Result<Json, Error> {
        match method {
            "rebuild" => {
                let started = !self.running.replace(true);
                if started {
                    cx.spawn(rebuild(
                        cx.clone(),
                        self.builds.clone(),
                        self.running.clone(),
                    ));
                }
                Json::from_value(&started)
            }
            other => Err(Error::Invalid(format!("unknown method {other}"))),
        }
    }
}

Modules§

env
The plugin’s environment: the daemon user’s login environment reduced to HOME, USER, LOGNAME, PATH (the login shell’s), LANG, LC_*, TMPDIR and SHELL (all of it under machine.full). Children spawned through process inherit exactly these; the daemon’s own environment never reaches a plugin. No grant.
http
HTTP: over wasi:http in a component (the wasi feature), and through the test host natively. An HTTP client over wasi:http (wasi feature): any method, request headers and body, a timeout, the response’s status, headers and body, and a cap on how much body is read. Grant: fetch:<host> (or fetch:<scheme>://<host>[:<port>], network.full, machine.full); the host refuses any other host before a connection is made, and the plugin sees Error::GrantDenied { grant: "fetch:<host>" }.
net
WebSockets the host holds for the plugin (daemon-net). Grant: socket.connect:<host>:<port> for each server (wss:// only). News arrives as crate::Event::Plugin named net::WEBSOCKET_EVENT; net::SocketEvent::from_event reads it.
panes
Panes on the daemon’s machine. Grants: panes.read, panes.write.
process
Spawn programs on the daemon’s machine. Grant: process.exec:<program> naming the program exactly as spawned (process.exec:* for any), or machine.full. Children are killed when the plugin stops.
watch
File and directory change notifications, debounced by the host and delivered as Event::FileChanged. Grant: fs.read:<path> covering the path. At most 256 watches per plugin.

Structs§

Caller
Who made a call, as the account routed it.
Context
A daemon plugin’s handle to the host, its configuration and its executor. Cheap to clone.
Interest
Which of the plugin’s UI surfaces some viewer on the account shows now: the union over every viewer, kept by the account and pushed to the daemon half whenever it changes (a surface shown or hidden, a viewer that opens or goes away). A daemon half that reads an outside source only for its UI reads it while Interest::any holds and stops when it goes away: nothing on screen, nothing read. Instances count as their surface (workers@<machine> is workers).
NextEvent
The next event the host delivers.
NextEventUntil
The next event, or None once the daemon’s clock reaches a deadline (Context::next_event_until).
Release
The Standard Code build a daemon runs (Context::release).
Sleep
Completes at an instant on the daemon’s clock. Dropped before it completes (it lost a race with an event), it takes its timer with it, so the host is not asked to drive the plugin at an instant nobody waits for.

Enums§

PaneChangeKind
How a pane changed (Event::PaneChanged).

Constants§

EVENT_QUEUE
Events kept while nothing awaits them.
INTEREST_EVENT
The plugin event the host delivers the viewers’ interest as ({ "surfaces": [...] }); the runtime turns it into Event::Interest.

Traits§

DaemonPlugin
A daemon (or companion) plugin.