Help us shape the future of Go! Take the 2026 Go Developer Survey

vero

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2026 License: MIT Imports: 18 Imported by: 0

README

vero

A Go backend with native macOS, Windows and Linux frontends. All built on macOS.

Quickstart on macOS · The go worker · macOS · Windows · Linux · Run the examples


macOS — SwiftUI, in the menu bar
Windows — WPF
Linux — GTK4

Quickstart on macOS

See all three running before building your own:

git clone https://github.com/calmdocs/vero && cd vero
./scripts/setup.sh                              # installs toolchains, builds everything
./scripts/run.sh --iso ~/Downloads/win11.iso    # opens all three

--iso is a Windows 11 ARM64 ISO, downloaded first from Microsoft. The third line installs it into a VM at ~/vm/vero-windows, which may take a bit of time and happens once: after that ./scripts/run.sh on its own opens all three, and the ISO can be deleted. Without the --iso ~/Downloads/win11.iso flag, macOS and Linux still open and Windows is skipped.

The go worker

All three apps below run this same Go program. Each section builds it for its own platform.

brew install go
mkdir -p ~/vero-example/worker && cd ~/vero-example/worker
go mod init worker
go get github.com/calmdocs/vero

main.go:

package main

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/calmdocs/vero"
)

// What the interface draws.
type Job struct {
	ID       int    `json:"id"`
	Name     string `json:"name"`
	Progress int    `json:"progress"`
}

type Status struct {
	Jobs []Job `json:"jobs"`
}

// What the interface asks for.  The name each is registered under is what
// routes it, and Swift names the same string.
type RestartJob struct {
	ID int `json:"id"`
}

var (
	mu   sync.Mutex
	jobs = []Job{{ID: 1, Name: "Photos"}, {ID: 2, Name: "Documents"}}
)

func snapshot() Status {
	mu.Lock()
	defer mu.Unlock()
	return Status{Jobs: append([]Job(nil), jobs...)}
}

func main() {
	// Everything the interface draws. vero pushes it whenever it changes, and
	// an Update handler replies with it.
	w := vero.NewWorker(vero.WorkerOptions{State: func() any { return snapshot() }})

	// The actual work.  Yours goes here.
	go func() {
		for range time.Tick(200 * time.Millisecond) {
			mu.Lock()
			for i := range jobs {
				if jobs[i].Progress < 100 {
					jobs[i].Progress++
				}
			}
			mu.Unlock()
		}
	}()

	r := vero.NewRouter()

	// Add a job.  Update replies with the new state, so the interface cannot
	// draw the list from before its own change.
	vero.Update(r, "addJob", func(_ context.Context, _ struct{}) error {
		mu.Lock()
		n := len(jobs) + 1
		jobs = append(jobs, Job{ID: n, Name: fmt.Sprintf("Job %d", n)})
		mu.Unlock()
		return nil
	})

	// Send one back to the beginning.
	vero.Update(r, "restartJob", func(_ context.Context, req RestartJob) error {
		mu.Lock()
		for i := range jobs {
			if jobs[i].ID == req.ID {
				jobs[i].Progress = 0
			}
		}
		mu.Unlock()
		return nil
	})

	// Blocks until the interface goes away, then returns so this process can
	// leave with it.
	w.Serve(r)
}

macOS: Add vero to your own macOS app

Install Xcode from the App Store. Steps 2 and 3 happen in it, and it brings the lipo that step 1 uses.

1. Build the worker
cd ~/vero-example/worker
GOOS=darwin GOARCH=amd64 go build -o worker-amd64 && \
GOOS=darwin GOARCH=arm64 go build -o worker-arm64 && \
lipo -create worker-amd64 worker-arm64 -output worker
2. The macOS SwiftUI app

In Xcode, File -> New -> Project -> macOS -> App, with Interface set to SwiftUI. Then:

  • File -> Add Package Dependencies... -> https://github.com/calmdocs/vero
  • drag ~/vero-example/worker/worker into the project, ticking your app under Add to targets

Replace ContentView.swift with this:

import SwiftUI
import Vero

// The same two types, and the same request names, as the worker.
struct Job: Decodable, Identifiable {
    let id: Int
    let name: String
    let progress: Int
}

struct Status: Decodable {
    let jobs: [Job]
}

struct AddJob: NamedRequest {
    static let name = "addJob"
    typealias Reply = Status
}

struct RestartJob: NamedRequest {
    static let name = "restartJob"
    typealias Reply = Status
    let id: Int
}

struct ContentView: View {
    // Copies the worker out of the app bundle, launches it, restarts it if it
    // dies, stops it when the app exits, and republishes this view whenever
    // the worker pushes a new Status.
    @StateObject private var vero = VeroModel<Status>(
        bundledWorker: "worker", directoryName: "Example/bin")

    var body: some View {
        VStack(spacing: 0) {
            HStack {
                Button("Add job") { vero.call(AddJob()) }
                    .disabled(vero.isBusy)
                Spacer()
                if let problem = vero.problem {
                    Text(problem).foregroundStyle(.orange)
                }
            }
            .padding(8)

            List(vero.state?.jobs ?? []) { job in
                HStack {
                    Button { vero.call(RestartJob(id: job.id)) } label: {
                        Image(systemName: "arrow.clockwise")
                    }
                    .disabled(vero.isBusy)

                    Text(job.name)
                    ProgressView(value: Double(job.progress) / 100)
                }
            }
        }
        .frame(minWidth: 320, minHeight: 240)
    }
}
3. Run it

Press Cmd-R. Two jobs appear and their progress climbs. Add job puts a third in the list. The refresh button beside a job sets that job's progress back to zero.

Windows: Add the same worker to a Windows app

Every step below runs on your Mac. Only step 4 needs a Windows on ARM machine, and step 4 can boot one for you on macOS.

Install the .NET SDK and a Windows ARM64 C compiler, once:

curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 8.0
export PATH="$HOME/.dotnet:$PATH"

mkdir -p ~/toolchains && cd ~/toolchains
curl -L "$(curl -s https://api.github.com/repos/mstorsjo/llvm-mingw/releases/latest \
    | grep -o 'https://[^"]*ucrt-macos-universal.tar.xz')" | tar -xJ
mv llvm-mingw-*-ucrt-macos-universal llvm-mingw
1. Build the worker and the library
cd ~/vero-example/worker
CGO_ENABLED=1 GOOS=windows GOARCH=arm64 \
    CC=$HOME/toolchains/llvm-mingw/bin/aarch64-w64-mingw32-clang \
    go build -buildmode=c-shared -o vero.dll github.com/calmdocs/vero/cshim
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -o worker.exe .
2. The Windows WPF app

Run these:

mkdir ../wpf-app && cd ../wpf-app
curl -O https://raw.githubusercontent.com/calmdocs/vero/main/bindings/csharp/Vero.cs

Add these four files to the same directory:

VeroExample.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
    <Nullable>enable</Nullable>
    <RootNamespace>VeroExample</RootNamespace>
  </PropertyGroup>
</Project>

App.xaml:

<Application x:Class="VeroExample.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml"/>

MainWindow.xaml:

<Window x:Class="VeroExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="vero" Width="360" Height="260">
    <DockPanel Margin="8">
        <Button x:Name="AddJob" Content="Add job" Click="AddJob_Click"
                DockPanel.Dock="Top" HorizontalAlignment="Left" Padding="8,2"/>
        <ItemsControl x:Name="Jobs" Margin="0,8,0,0">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <DockPanel Margin="0,4">
                        <Button Content="&#x21bb;" Tag="{Binding Id}" Click="Restart_Click"/>
                        <TextBlock Text="{Binding Name}" Width="90" Margin="8,0"/>
                        <ProgressBar Value="{Binding Progress}" Maximum="100" Height="12"/>
                    </DockPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </DockPanel>
</Window>

MainWindow.xaml.cs:

using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows;
using System.Windows.Controls;
using Vero;

namespace VeroExample;

// The same types, and the same request names, as the worker.
public record Job(
    [property: JsonPropertyName("id")]       int Id,
    [property: JsonPropertyName("name")]     string Name,
    [property: JsonPropertyName("progress")] int Progress);

public record Status(
    [property: JsonPropertyName("jobs")] Job[] Jobs);

public partial class MainWindow : Window
{
    private readonly VeroClient _vero;
    private readonly ObservableCollection<Job> _jobs = new();

    public MainWindow()
    {
        InitializeComponent();
        Jobs.ItemsSource = _jobs;

        // Launches the worker beside the executable, restarts it if it dies,
        // and stops it when this process exits.
        _vero = new VeroClient(Path.Combine(
            AppDomain.CurrentDomain.BaseDirectory, "worker.exe"));

        _ = ReadEvents();
    }

    // Pushed the instant the worker's state moves, on its own thread.
    private async System.Threading.Tasks.Task ReadEvents()
    {
        await foreach (var element in _vero.Events())
        {
            var status = element.Deserialize<Status>();
            if (status is not null)
                Dispatcher.Invoke(() => Apply(status));
        }
    }

    private void Apply(Status status)
    {
        _jobs.Clear();
        foreach (var job in status.Jobs) _jobs.Add(job);
    }

    private async void AddJob_Click(object sender, RoutedEventArgs e) =>
        await _vero.CallAsync("addJob", new { });

    private async void Restart_Click(object sender, RoutedEventArgs e) =>
        await _vero.CallAsync("restartJob", new { id = (int)((Button)sender).Tag });
}
3. Build the app
dotnet publish -c Release -r win-arm64 --self-contained \
    -p:EnableWindowsTargeting=true -o out
cp ../worker/vero.dll ../worker/worker.exe out/
4. Run it

On a Windows on ARM machine, run VeroExample.exe from out/.

No Windows machine? Boot one on your Mac. The first time, install Windows into the VM from a Microsoft ISO. The install is unattended, and happens once:

brew install qemu
git clone https://github.com/calmdocs/vero
vero/scripts/run-windows.sh --iso ~/Downloads/win11.iso --install --payload out

After that, this boots the same VM with your latest build on a disc:

vero/scripts/run-windows.sh --payload out

Windows opens in a window on your Mac, which you use like any other. In it, copy the vero folder from the CD drive to C:\, and run VeroExample.exe inside it. Two jobs appear and their progress climbs. Add job puts a third in the list. The refresh button beside a job sets that job's progress back to zero.

Linux: Add the same worker to a Linux app

libvero.so has to be compiled on Linux, so step 1 builds it in a container. Everything else runs on your Mac.

Install colima and Docker once:

brew install colima docker && colima start
1. Build the worker and the library
cd ~/vero-example/worker
docker run --rm -v "$PWD":/src -w /src \
    -e GOCACHE=/tmp/gocache -e GOPATH=/tmp/go -e GOTOOLCHAIN=auto \
    golang:1.24-bookworm \
    sh -c 'CGO_ENABLED=1 go build -buildmode=c-shared -o libvero.so github.com/calmdocs/vero/cshim'

CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o worker-linux .
2. The Linux GTK4 app

Run these:

mkdir ../gtk-app && cd ../gtk-app
curl -O https://raw.githubusercontent.com/calmdocs/vero/main/bindings/python/vero.py
cp ../worker/libvero.so .
cp ../worker/worker-linux worker

Then add main.py:

#!/usr/bin/env python3
import os
import gi

gi.require_version("Gtk", "4.0")
from gi.repository import GLib, Gtk

from vero import Vero, run_in_thread

HERE = os.path.dirname(os.path.abspath(__file__))


class Window(Gtk.ApplicationWindow):
    def __init__(self, app):
        super().__init__(application=app, title="vero", default_width=360)
        self.bars = {}

        # Launches the worker beside this file, restarts it if it dies, and
        # stops it when this process exits.
        self.vero = Vero(os.path.join(HERE, "libvero.so"),
                         os.path.join(HERE, "worker"))

        add = Gtk.Button(label="Add job")
        add.connect("clicked", lambda _: self.vero.call("addJob", {}))

        self.rows = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8,
                      margin_top=8, margin_bottom=8, margin_start=8, margin_end=8)
        box.append(add)
        box.append(self.rows)
        self.set_child(box)

        # latest draws a window that has just opened; events keep it current.
        if status := self.vero.latest():
            self.apply(status)
        run_in_thread(self.vero, lambda s: GLib.idle_add(self.apply, s))

    def apply(self, status):
        for job in status["jobs"]:
            if job["id"] not in self.bars:
                self.bars[job["id"]] = self.add_row(job)
            self.bars[job["id"]].set_fraction(job["progress"] / 100)
        return False  # GLib.idle_add: run once

    def add_row(self, job):
        bar = Gtk.ProgressBar(hexpand=True, valign=Gtk.Align.CENTER)
        restart = Gtk.Button(icon_name="view-refresh-symbolic")
        restart.connect(
            "clicked", lambda _, i=job["id"]: self.vero.call("restartJob", {"id": i}))

        row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        row.append(restart)
        row.append(Gtk.Label(label=job["name"], width_chars=10, xalign=0))
        row.append(bar)
        self.rows.append(row)
        return bar


app = Gtk.Application(application_id="com.example.vero")
app.connect("activate", lambda a: Window(a).present())
app.run(None)
3. Run it

On a Linux machine:

sudo apt-get install -y python3-gi gir1.2-gtk-4.0
chmod +x main.py && ./main.py

Or run it from your Mac, on a virtual display in a container, and watch it in Screen Sharing:

docker build -t vero-gtk - <<'EOF'
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
      python3 python3-gi gir1.2-gtk-4.0 libgtk-4-1 xvfb x11vnc xauth \
    && rm -rf /var/lib/apt/lists/*
EOF

docker run --rm -p 5901:5900 -v "$PWD":/app -w /app vero-gtk sh -c '
    Xvfb :99 -screen 0 480x440x24 &
    sleep 2
    export DISPLAY=:99
    python3 main.py &
    sleep 4
    x11vnc -display :99 -forever -nopw -listen 0.0.0.0'

open vnc://localhost:5901

Two jobs appear and their progress climbs. Add job puts a third in the list. The refresh button beside a job sets that job's progress back to zero.

Run the examples

example/menubar-app macOS, SwiftUI
example/wpf-app Windows, WPF
example/gtk-app Linux, GTK4

The repository's own tests:

go test -race ./...
cd bindings/python && python3 -m unittest
swift build

Licence

MIT

Documentation

Overview

Package vero builds desktop applications whose logic is written once, in Go, and whose interface is the platform's own toolkit - SwiftUI on macOS, WinUI on Windows, GTK on Linux.

The Go GUI field splits three ways: Fyne and Gio draw their own widgets, so the result looks the same everywhere and native nowhere; Wails and Lorca wrap a webview, which is Electron's tradeoff with a smaller binary; Walk is Windows only. vero takes the fourth path. The interface is genuinely the system's, and Go supplies everything behind it.

The shape of a vero application

Your logic lives in a worker: an ordinary Go program that either runs on its own, as a daemon on a headless server, or is launched by a GUI. The same binary does both, and it does not know which it is doing beyond one flag.

worker  ->  your logic; runs headless, or behind an interface
        ->  a Supervisor launches it, restarts it, and talks to it
        ->  a native interface drives the Supervisor through five C functions

The channel between the interface and the worker is the pipes the operating system already gave you when the supervisor launched it. That is not a smaller version of a socket, it is a different guarantee: there is no filesystem object, so nothing else on the machine can connect even in principle, and the channel is destroyed with the process. It also means a worker cannot outlive the interface that started it - when the application quits, or crashes, or is force quit, the worker's standard input closes and it exits, with no PID file and no heartbeat to get wrong.

Crashes

The worker is a separate process, so a panic in it - or one of Go's unrecoverable runtime failures, like a concurrent map write, which recover cannot catch - takes down the worker and not your interface. The supervisor notices, reports it, and starts a new one.

The wire format

Newline delimited JSON, in both directions. Your own message travels in "p", untouched:

to the worker     {"id":7,"p":<your request>}
from the worker   {"t":"reply","id":7,"p":<your reply>}
                  {"t":"reply","id":7,"e":"what went wrong"}
                  {"t":"event","p":<your event>}

Events are unsolicited: the worker sends one whenever its state moves, so an interface never polls and never waits on a timer. Replies are matched to requests by id, so a slow request cannot hold up the events behind it.

Standard output carries this stream and nothing else. Logs go to standard error, always. A worker that no one is watching writes neither.

Index

Constants

View Source
const DefaultStateInterval = 100 * time.Millisecond

DefaultStateInterval is how often WorkerOptions.State is sampled for a change when StateInterval is not set. Fast enough that a progress bar moves smoothly, slow enough that nothing is sent while nothing is happening.

View Source
const MaxLineSize = 16 << 20 // 16 MiB

MaxLineSize caps one protocol line, so a corrupt or hostile stream cannot ask for an unbounded allocation. Payloads larger than this belong in a file with the path sent through the pipe.

Variables

View Source
var ErrAlreadyRunning = errors.New("a worker is already running for this application")

ErrAlreadyRunning is returned when another process is already supervising this worker.

Two copies of an application - the one in /Applications and the one still in ~/Downloads after an update - are two processes, and without this they would each start a worker and both would work on the same state. One window too many is a nuisance; two workers is corruption.

View Source
var ErrWorkerNotRunning = errors.New("vero: the worker is not running")

ErrWorkerNotRunning is returned by Request when the worker has died and has not been restarted yet. It is worth distinguishing in an interface: it means wait, not that anything you did was wrong.

View Source
var ShutdownGrace = 5 * time.Second

ShutdownGrace is how long Run waits for handlers to notice that the interface has gone before exiting regardless.

Functions

func Handle

func Handle[Req any, Rep any](r *Router, name string, fn func(ctx context.Context, request Req) (Rep, error))

Handle registers a handler for one request name.

Req is what the request decodes into and Rep is what the reply is encoded from, so neither the handler nor the caller has to guess. Use struct{} for a request that carries nothing.

It is a function rather than a method because Go does not allow methods to introduce type parameters.

func Update added in v0.3.0

func Update[Req any](r *Router, name string, fn func(ctx context.Context, request Req) error)

Update registers a handler that changes the worker's state and replies with it, so the interface cannot draw the state from before its own change.

The reply is WorkerOptions.State, which Serve hands to the router, and the handler itself returns only an error. Use Handle when the reply is something other than the state.

Types

type Envelope

type Envelope struct {
	// Kind is "event" or "reply" on the way out of a worker, and empty on the
	// way in, where every line is a request.
	Kind string `json:"t,omitempty"`

	// ID matches a reply to the request that caused it.  Events have none.
	ID uint64 `json:"id,omitempty"`

	// Name routes a request to a handler, when a Router is used.  Empty when
	// the worker has a single handler that dispatches for itself.
	//
	// It lives here rather than inside the payload so that vero can route
	// without knowing anything about the message, and so an application is
	// not obliged to carry a type field in every request it defines.
	Name string `json:"n,omitempty"`

	// Payload is your own message, carried through untouched.
	Payload json.RawMessage `json:"p,omitempty"`

	// Error is set instead of Payload when a handler returned an error.
	Error string `json:"e,omitempty"`
}

Envelope is one line of the protocol. Everything is optional except what the message kind requires, so the same struct decodes in both directions.

type Handler

type Handler func(ctx context.Context, request json.RawMessage) (reply any, err error)

Handler answers one request.

ctx is cancelled when the interface goes away, so a handler that waits - a long poll, most obviously - must select on ctx.Done() as well. Without that the worker cannot exit when its parent does: it has stopped reading requests, but it is still waiting for the answer to one nobody will read. Returning an error sends the message to the caller rather than closing anything: the interface can then tell "the worker refused this" from "the worker is not there", which want opposite responses.

Returning a nil reply and a nil error answers nothing at all, which suits a request that only causes an action.

type RemoteError

type RemoteError struct{ Message string }

RemoteError is what a Handler's error becomes on the other side of the pipe.

func (*RemoteError) Error

func (e *RemoteError) Error() string

type Router

type Router struct {
	// contains filtered or unexported fields
}

Router sends each request to a handler chosen by name, with the request and the reply as the types that handler actually wants.

Without it a worker takes one Handler and dispatches for itself, which means a switch on a type field, a json.Unmarshal per branch, and a caller that has to work out what came back. That last part is where it goes wrong: an application ends up trying to decode a reply as one type, then another, until one succeeds - and an empty JSON array decodes as any of them, so a list that happens to be empty is read as whatever was tried first.

r := vero.NewRouter()
vero.Handle(r, "getGroups", func(ctx context.Context, _ struct{}) ([]Group, error) {
    return groups(), nil
})
vero.Update(r, "addGroup", func(ctx context.Context, req AddGroup) error {
    return add(req.Name)
})
w.Serve(r)

The name travels in the envelope, so an application's own messages stay exactly as it defined them.

func NewRouter

func NewRouter() *Router

NewRouter returns an empty Router.

func (*Router) Fallback

func (r *Router) Fallback(h Handler)

Fallback answers requests whose name matches nothing registered, including requests that carry no name at all.

Without one an unknown name is refused, which is the default and usually right: it is what makes named routing the only way in. Set one only while something older is still expected to connect, and watch FallbackCalls to know when that has stopped being true. It is worth setting when an interface can be newer than the worker it is driving: it then asks for things this build has never heard of, and refusing is a failure the person did not cause and cannot act on.

func (*Router) FallbackCalls

func (r *Router) FallbackCalls() uint64

FallbackCalls counts the requests that have reached the fallback.

It answers the question a fallback creates: when can it go? A fallback is there for interfaces that predate named routing, and while any of them are still running this climbs. When it stays at zero across a release, nothing is using the untyped path and both it and the fallback can be deleted - which is what makes typed routing the only way in.

func (*Router) Names

func (r *Router) Names() []string

Names lists what has been registered, in no particular order.

type State

type State int

State is what the supervisor last saw the worker doing.

const (
	// Starting means a worker is being launched, including a restart.
	Starting State = iota
	// Running means a worker is up and answering.
	Running
	// Restarting means one died and another is on the way.
	Restarting
	// Stopped means Stop was called; nothing further will be launched.
	Stopped
)

func (State) String

func (s State) String() string

type Supervisor

type Supervisor struct {
	// contains filtered or unexported fields
}

Supervisor runs a worker, keeps it running, and talks to it.

func Supervise

func Supervise(opts SupervisorOptions) *Supervisor

Supervise launches the worker and keeps it alive until Stop.

It returns immediately; the worker comes up on its own goroutine. Requests made before it is ready return ErrWorkerNotRunning rather than blocking, so an interface can draw itself at once and show that it is waiting.

func (*Supervisor) Call

func (s *Supervisor) Call(ctx context.Context, name string, request any) (json.RawMessage, error)

Call sends a request to the handler registered under name, for a worker using a Router.

The name travels beside the payload rather than inside it, so an application's own message types stay exactly as it defined them.

func (*Supervisor) Err

func (s *Supervisor) Err() error

Err reports why the worker never started, or nil.

The one that matters is ErrAlreadyRunning: another process holds the lock, so this Supervisor will not run a worker at all.

func (*Supervisor) Latest

func (s *Supervisor) Latest() json.RawMessage

Latest returns the most recent event, or nil before the first one.

An interface that has just drawn a window needs the current state without waiting for the next change, and this is that. Events keep it up to date afterwards.

func (*Supervisor) Request

func (s *Supervisor) Request(ctx context.Context, request any) (json.RawMessage, error)

Request sends a request and waits for its reply.

A handler's error comes back as *RemoteError, which is worth telling apart from ErrWorkerNotRunning: one means the worker refused what you asked, the other that it is not there to ask.

func (*Supervisor) Restarts

func (s *Supervisor) Restarts() int

Restarts counts how many times the worker has been relaunched after dying.

func (*Supervisor) State

func (s *Supervisor) State() State

State reports what the worker is doing now.

func (*Supervisor) Stop

func (s *Supervisor) Stop() error

Stop shuts the worker down and waits for it to go.

Closing its standard input is the whole shutdown protocol: Run returns, the process exits, and anything it was holding is released the way it would be on any other exit. A worker that ignores that is killed after a grace period.

type SupervisorOptions

type SupervisorOptions struct {
	// Path is the worker binary.  Args are passed to it verbatim, so the
	// application keeps its own command line.
	Path string
	Args []string

	// Dir and Env, if set, are the worker's working directory and
	// environment.  VERO_SERVE is added to Env either way.
	Dir string
	Env []string

	// OnEvent receives every event the worker emits, in order.
	OnEvent func(event json.RawMessage)

	// OnLog receives the worker's standard error, a line at a time.  Workers
	// log there precisely so this does not have to guess at framing.
	OnLog func(line string)

	// OnStateChange is told when the worker starts, dies, or is given up on.
	OnStateChange func(State)

	// Backoff is the wait before the first restart, doubling up to
	// MaxBackoff.  A worker that crashes on startup would otherwise be
	// relaunched as fast as the machine can fork.  Zero means 200ms and 30s.
	Backoff    time.Duration
	MaxBackoff time.Duration

	// Lock keys the single-worker lock: one process at a time supervises a
	// worker under a given key.  Empty means the worker's own path, which is
	// what you want unless two applications share a binary and should still
	// be allowed to run at once - then give them a key each.
	Lock string

	// NoLock turns the lock off, for a program that genuinely wants several
	// workers at once.
	NoLock bool
}

SupervisorOptions describes the worker to run and who to tell about it.

The callbacks are called from the supervisor's own goroutines, never on the interface's main thread. Whatever you do in them, hop to the main thread before touching the interface.

type Worker

type Worker struct {
	// contains filtered or unexported fields
}

Worker is the logic half of a vero application.

func NewWorker

func NewWorker(opts WorkerOptions) *Worker

NewWorker prepares a worker. Whether a supervisor launched it is read from the environment, so the same binary and the same code run both ways.

Call this before starting any goroutine

In serve mode standard output belongs to the protocol, so a stray Println anywhere in the program would corrupt a frame. Rather than ask every author of every line in the program to remember that, this takes the real standard output for itself and points the package-level os.Stdout at standard error.

fmt.Println and friends read os.Stdout at the moment they are called, so every existing print in the program - and every one anybody adds later, in any package, including ones you depend on and cannot edit - lands harmlessly on stderr from here on. There is nothing to audit and nothing to keep auditing.

It does mean reassigning a package-level variable, so call NewWorker before anything else could be printing: another goroutine writing while this reassigns is a data race, and one the race detector will find.

func (*Worker) Emit

func (w *Worker) Emit(event any)

Emit sends an event to whoever is listening. When nothing is - a daemon running with no interface and no -json - it costs a comparison and returns.

Call it whenever state moves. That is what lets an interface show progress without polling: there is no timer to tune and nothing is sent while the worker is quiet.

func (*Worker) EmitOnChange

func (w *Worker) EmitOnChange(ctx context.Context, interval time.Duration, snapshot func() any)

EmitOnChange emits what snapshot returns, whenever it differs from what was emitted last. It runs until ctx is done, so start it with go.

go w.EmitOnChange(ctx, 250*time.Millisecond, func() any { return state() })

Most workers do not call this: set WorkerOptions.State instead and Serve runs it for you, on a context that ends when the interface does. This is for the ones whose events are not one state snapshot.

Emitting on a timer instead is the obvious thing to write and it defeats the point: an interface is sent the same thing several times a second, and the quiet-when-nothing-happens property - the reason events are pushed rather than polled - is gone.

Comparison is by reflect.DeepEqual, so snapshot should return a value rather than something holding a pointer to state that keeps changing underneath it.

func (*Worker) Log

func (w *Worker) Log(format string, a ...any)

Log writes a line to standard error, where logs belong. Standard output is reserved for the protocol, and a stray write there corrupts the stream.

func (*Worker) Run

func (w *Worker) Run(h Handler) error

Run answers requests until the other end goes away, then returns nil.

When no supervisor launched this worker there is nothing to answer, so Run blocks forever and the worker just works. When one did, standard input closing means the interface has quit - or crashed, or been force quit - and Run returns so the process can exit with it. That is the whole of the lifecycle: no PID file, no heartbeat, no orphan.

func (*Worker) Serve

func (w *Worker) Serve(r *Router) error

Serve answers requests by name, using a Router.

Otherwise identical to Run: it blocks, it returns when the interface goes away, and on its own it never returns.

func (*Worker) Supervised

func (w *Worker) Supervised() bool

Supervised reports whether a Supervisor launched this worker. Useful when the standalone form wants to behave differently - printing a summary and exiting, say, rather than running forever.

type WorkerOptions

type WorkerOptions struct {
	// JSON emits the event stream on standard output.  A Supervisor turns
	// this on for you; set it yourself when a script is watching.
	JSON bool

	// Quiet suppresses the human readable log on standard error.
	Quiet bool

	// Version is what -version reports.  An interface that ships this worker
	// in its bundle asks for it to decide whether the copy it has on disk is
	// older than the one it was built with, so the two halves have to agree
	// on the flag: vero registers it here so they cannot drift.
	Version string

	// State is everything the interface draws, as one value.  Set it and the
	// worker pushes it whenever it changes - Serve starts the emitter and
	// stops it - and a handler registered with Update replies with it.
	//
	// Leave it nil to emit by hand with Emit or EmitOnChange, for a worker
	// whose events are not one snapshot.
	State func() any

	// StateInterval is how often State is sampled for a change.  Zero means
	// DefaultStateInterval.
	StateInterval time.Duration

	// ShowVersion is set by RegisterFlags when -version was passed. Answer it
	// and exit before anything else starts - it runs as its own short-lived
	// process, several times per launch, and must not open a port, touch a
	// configuration directory, or print anything else.
	ShowVersion bool
}

WorkerOptions controls what a worker writes, and where.

The two settings are deliberately separate. Format and volume are different questions: an interface wants every state change as JSON, and a person reading a terminal or a journal wants a handful of lines saying what actually happened. Emitting the first to satisfy the second is how logs become unreadable.

func (*WorkerOptions) PrintVersionAndExit

func (o *WorkerOptions) PrintVersionAndExit()

PrintVersionAndExit answers -version if it was asked for, and does not return in that case.

Call it immediately after flag.Parse, before anything else starts.

func (*WorkerOptions) RegisterFlags

func (o *WorkerOptions) RegisterFlags(fs *flag.FlagSet)

RegisterFlags adds -json and -quiet to fs. Call it before flag.Parse.

A worker does not need a flag for serve mode: a Supervisor sets it through the environment, so nothing on the command line has to describe an arrangement the user did not ask for.

Directories

Path Synopsis
Command cshim builds vero as a C archive, so a native interface can embed it and drive a worker without knowing anything about pipes, JSON framing or process supervision.
Command cshim builds vero as a C archive, so a native interface can embed it and drive a worker without knowing anything about pipes, JSON framing or process supervision.
example
worker command
Command worker is the logic half of the example: the part you would write.
Command worker is the logic half of the example: the part you would write.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL