Plugins

Applications that embed Scriptling can use the same executable plugin system as the CLI. Create one plugin.Manager for the application, load plugin directories during startup, and register plugin libraries with each Scriptling environment you create.

package main

import (
    "context"
    "log"

    logslog "github.com/paularlott/logger/slog"

    "github.com/paularlott/scriptling"
    "github.com/paularlott/scriptling/plugin"
)

func main() {
    ctx := context.Background()

    appLogger := logslog.New(logslog.Config{
        Level:  "info",
        Format: "console",
    })

    manager := plugin.NewManager(appLogger, func(name string, err error) {
        log.Println("plugin crashed:", name, err)
        // Decide whether to terminate, restart, or mark this host unhealthy.
    })
    manager.AddDir("./plugins")
    if err := manager.Load(ctx); err != nil {
        log.Fatal(err)
    }
    defer manager.Close()

    for _, warning := range manager.Warnings() {
        log.Println("plugin warning:", warning)
    }

    env := scriptling.New()
    plugin.RegisterLibraries(env, manager)

    _, err := env.Eval(`
import plugin.hello
print(plugin.hello.greet("Ada"))
`)
    if err != nil {
        log.Fatal(err)
    }
}

Environment Model

Each Scriptling environment belongs to one Go thread of execution and must not be evaluated concurrently. Create one environment per concurrent request or worker, then call plugin.RegisterLibraries(env, manager) for each environment.

The manager starts each plugin executable once. Multiple environments can share that manager, and plugin calls from those separate environments may overlap on the same plugin process. The stdio JSON-RPC connection multiplexes overlapping calls by request id; connection pooling is intentionally not used because it would create multiple plugin process instances and violate the singleton plugin model.

Plugin Logs

Pass a logger to plugin.NewManager(appLogger, crashHandler) to install the manager-lifetime host logger used for records emitted by Go plugins through plugin.Logger(ctx). Pass the same github.com/paularlott/logger.Logger your application already uses; the example above uses github.com/paularlott/logger/slog so plugin logs are visible during development. manager.SetLogger() is also available for late wiring. If no logger is configured, plugin log records are acknowledged and dropped.

A Go plugin writes to the host logger from an active plugin call:

plugin.Logger(ctx).Info("plugin work started", "name", name)

Crash Handling

Pass a crash handler to plugin.NewManager(appLogger, crashHandler) to handle plugin processes that exit unexpectedly after loading. manager.SetCrashHandler() is also available for late wiring. This is the normal long-running server hook for logging, terminating, restarting, or marking the host unhealthy.

manager.SetCrashHandler(func(name string, err error) {
    log.Println("plugin crashed:", name, err)
})

manager.Health() reports plugin processes that have exited or whose stdio transport has closed. The returned map is empty when all loaded plugins are healthy.

Startup and Failure Behavior

Plugins are loaded eagerly. Missing or invalid executables become manager warnings, which lets applications decide how visible those startup problems should be. A runtime RPC failure from a plugin call is returned as the script error for that call.

More Detail

  • Plugin Manager covers the same pattern from the plugin documentation section.
  • Go Plugins explains how to write a plugin executable in Go.
  • JSON-RPC Protocol documents the stdio protocol for non-Go plugins.