Skip to content

Embedded Daemon

Run the wmux daemon as an in-process goroutine. This is the simplest deployment mode -- no external processes, no child management. The trade-off is that sessions are lost when the host process exits.

When to use

  • Web servers that expose terminals to browser clients
  • Development tools where session persistence is not required
  • Test harnesses that need disposable PTY sessions

Full example

package main

import (
    "context"
    "log"
    "os"
    "os/signal"
    "syscall"

    "github.com/wblech/wmux/pkg/client"
    "github.com/wblech/wmux/addons/charmvt"
)

func main() {
    ctx, cancel := signal.NotifyContext(context.Background(),
        os.Interrupt, syscall.SIGTERM)
    defer cancel()

    // 1. Create and start the embedded daemon
    d, err := client.NewDaemon(
        client.WithNamespace("myapp"),
        charmvt.Backend(),
        client.WithColdRestore(true),
    )
    if err != nil {
        log.Fatal(err)
    }

    daemonErr := make(chan error, 1)
    go func() {
        daemonErr <- d.Serve(ctx)
    }()

    // 2. Wait for the socket to become ready
    c, err := client.New(
        client.WithNamespace("myapp"),
        client.WithAutoStart(false),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()

    // 3. Use the client
    info, err := c.Create("main", client.CreateParams{
        Cols: 120,
        Rows: 40,
    })
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("session %s (pid %d)", info.ID, info.Pid)

    // Wait for shutdown
    select {
    case err := <-daemonErr:
        if err != nil {
            log.Fatal(err)
        }
    case <-ctx.Done():
    }
}

Socket readiness

With WithAutoStart(false), client.New() attempts a single connection to the daemon socket. It does not poll or retry -- if the socket is not ready, New returns an error immediately. When embedding the daemon in-process, use a retry loop or poll the socket before calling New:

var c *client.Client
for range 30 {
    c, err = client.New(
        client.WithNamespace("myapp"),
        client.WithAutoStart(false),
    )
    if err == nil {
        break
    }
    time.Sleep(100 * time.Millisecond)
}

When the daemon is embedded, the socket is typically ready in under 10 ms.

Note

With WithAutoStart(true) (the default), the client handles daemon startup and socket readiness internally. The retry loop is only needed for embedded mode where you start the daemon goroutine yourself.

Warm attach

Because the daemon lives in the same process, attaching to an existing session returns instantly with the current viewport snapshot:

result, err := c.Attach("main")
if err != nil {
    log.Fatal(err)
}

// result.Snapshot.Replay contains the unified terminal replay bytes
// (scrollback history followed by viewport state in a single stream).
log.Printf("attached to %s (%d x %d)",
    result.Session.ID, result.Session.Cols, result.Session.Rows)

The snapshot is populated when an emulator addon is configured (e.g., charmvt.Backend()). Without an addon, Replay is nil.

Available backends

charmvt (recommended) — Pure Go, no external dependencies:

import "github.com/wblech/wmux/addons/charmvt"

d, err := client.NewDaemon(
    charmvt.Backend(),
)

Install: go get github.com/wblech/wmux/addons/charmvt

The scrollback size can be changed at runtime on a live session:

err := c.UpdateEmulatorScrollback("main", 50000)

The charmvt backend is the recommended and only maintained emulator addon. The xterm Node.js addon was removed in favour of the pure-Go charmvt emulator (see ADR 0027).