plugin API

plugin

package

API reference for the plugin package.

F
function

LoadSo

LoadSo loads a Go plugin (.so) exposing a PluginFactory symbol.

Parameters

path
string

Returns

error
core/plugin/loader_so.go:9-27
func LoadSo(path string) (Factory, error)

{
	p, err := stdplugin.Open(path)
	if err != nil {
		return nil, err
	}

	symbol, err := p.Lookup("PluginFactory")
	if err != nil {
		return nil, err
	}

	if factory, ok := symbol.(func() Plugin); ok {
		return factory, nil
	}
	if factory, ok := symbol.(*func() Plugin); ok {
		return *factory, nil
	}
	return nil, errors.New("plugin: PluginFactory has wrong type")
}
I
interface

Plugin

Plugin defines the lifecycle interface for all plugins.

core/plugin/plugin.go:16-20
type Plugin interface

Methods

Name
Method

Returns

string
func Name(...)
Start
Method

Returns

error
func Start(...)
Stop
Method

Returns

error
func Stop(...)
T
type

Factory

Factory is a function that creates a new plugin instance.

core/plugin/plugin.go:23-23
type Factory func() Plugin
S
struct

Registry

Registry manages plugin registration and lifecycle in a deterministic order.

core/plugin/registry.go:8-14
type Registry struct

Methods

Register
Method

Register adds a plugin to the registry.

Parameters

p Plugin

Returns

error
func (*Registry) Register(p Plugin) error
{
	r.mu.Lock()
	defer r.mu.Unlock()

	name := p.Name()
	if _, ok := r.byName[name]; ok {
		return ErrAlreadyRegistered
	}

	r.byName[name] = p
	r.order = append(r.order, name)
	return nil
}
Unregister
Method

Unregister removes a plugin by name.

Parameters

name string
func (*Registry) Unregister(name string)
{
	r.lifecycleMu.Lock()
	defer r.lifecycleMu.Unlock()

	r.mu.Lock()
	plugin := r.byName[name]
	delete(r.byName, name)
	for i, current := range r.order {
		if current == name {
			r.order = append(r.order[:i], r.order[i+1:]...)
			break
		}
	}
	r.mu.Unlock()
	if r.running[name] {
		delete(r.running, name)
		if plugin != nil {
			_ = plugin.Stop()
		}
	}
}
Get
Method

Get returns a plugin and whether it exists.

Parameters

name string

Returns

bool
func (*Registry) Get(name string) (Plugin, bool)
{
	r.mu.RLock()
	defer r.mu.RUnlock()

	plugin, ok := r.byName[name]
	return plugin, ok
}
Names
Method

Names returns registered plugin names in insertion order.

Returns

[]string
func (*Registry) Names() []string
{
	r.mu.RLock()
	defer r.mu.RUnlock()

	names := make([]string, len(r.order))
	copy(names, r.order)
	return names
}
StartAll
Method

StartAll starts plugins in insertion order and returns any collected errors.

Returns

[]error
func (*Registry) StartAll() []error
{
	r.lifecycleMu.Lock()
	defer r.lifecycleMu.Unlock()

	names := r.Names()
	var errs []error
	var started []string
	for _, name := range names {
		if r.running[name] {
			continue
		}
		p, ok := r.Get(name)
		if !ok {
			continue
		}
		if err := p.Start(); err != nil {
			errs = append(errs, err)
			for i := len(started) - 1; i >= 0; i-- {
				startedPlugin, exists := r.Get(started[i])
				if exists {
					if stopErr := startedPlugin.Stop(); stopErr != nil {
						errs = append(errs, stopErr)
						continue
					}
				}
				delete(r.running, started[i])
			}
			return errs
		}
		r.running[name] = true
		started = append(started, name)
	}
	return errs
}
StopAll
Method

StopAll stops plugins in reverse insertion order and returns any collected errors.

Returns

[]error
func (*Registry) StopAll() []error
{
	r.lifecycleMu.Lock()
	defer r.lifecycleMu.Unlock()

	names := r.Names()
	var errs []error
	for i := len(names) - 1; i >= 0; i-- {
		name := names[i]
		if !r.running[name] {
			continue
		}
		p, ok := r.Get(name)
		if !ok {
			delete(r.running, name)
			continue
		}
		if err := p.Stop(); err != nil {
			errs = append(errs, err)
		} else {
			delete(r.running, name)
		}
	}
	return errs
}

DiscoverFromValues inspects provided values and registers any that implement Plugin.

Parameters

values ...interface{}

Returns

int
func (*Registry) DiscoverFromValues(values ...interface{}) int
{
	if r == nil {
		return 0
	}

	pluginType := reflect.TypeOf((*Plugin)(nil)).Elem()
	count := 0
	for _, value := range values {
		if value == nil {
			continue
		}

		t := reflect.TypeOf(value)
		if t.Implements(pluginType) {
			p, _ := value.(Plugin)
			if p != nil && r.Register(p) == nil {
				count++
			}
			continue
		}

		if t.Kind() == reflect.Ptr && t.Elem().Implements(pluginType) {
			pluginValue, _ := reflect.ValueOf(value).Elem().Interface().(Plugin)
			if pluginValue != nil && r.Register(pluginValue) == nil {
				count++
			}
		}
	}
	return count
}

Fields

Name Type Description
mu sync.RWMutex
lifecycleMu sync.Mutex
order []string
byName map[string]Plugin
running map[string]bool
F
function

NewRegistry

NewRegistry creates an empty plugin registry.

Returns

core/plugin/registry.go:17-22
func NewRegistry() *Registry

{
	return &Registry{
		byName:  make(map[string]Plugin),
		running: make(map[string]bool),
	}
}
S
struct

ExecSandbox

ExecSandbox runs a plugin as an external process using a simple JSON-over-stdio protocol.

core/plugin/sandbox.go:14-19
type ExecSandbox struct

Methods

Start
Method

Start launches the external process and waits for a ready signal.

Parameters

Returns

error
func (*ExecSandbox) Start(ctx context.Context) error
{
	e.mu.Lock()
	defer e.mu.Unlock()

	if e.cmd.Process != nil {
		return errors.New("plugin: sandbox already started")
	}

	in, err := e.cmd.StdinPipe()
	if err != nil {
		return err
	}
	out, err := e.cmd.StdoutPipe()
	if err != nil {
		return err
	}

	e.in = in
	e.out = out
	if err := e.cmd.Start(); err != nil {
		return err
	}

	decoder := json.NewDecoder(io.LimitReader(e.out, maxReadyMessageSize+1))
	ready := make(chan error, 1)
	go func() {
		var msg map[string]any
		if err := decoder.Decode(&msg); err != nil {
			ready <- err
			return
		}
		if ok, _ := msg["ready"].(bool); ok {
			ready <- nil
			return
		}
		ready <- errors.New("plugin: unexpected ready message")
	}()

	startCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	select {
	case err := <-ready:
		if err == nil {
			return nil
		}
		return errors.Join(err, e.killAndWait())
	case <-startCtx.Done():
		return errors.Join(startCtx.Err(), e.killAndWait())
	}
}
Stop
Method

Stop sends a stop command and waits for the external process to exit.

Returns

error
func (*ExecSandbox) Stop() error
{
	ctx, cancel := context.WithTimeout(context.Background(), defaultStopTimeout)
	defer cancel()
	return e.StopContext(ctx)
}
StopContext
Method

StopContext sends a stop command and enforces the supplied shutdown deadline.

Parameters

Returns

error
func (*ExecSandbox) StopContext(ctx context.Context) error
{
	e.mu.Lock()
	defer e.mu.Unlock()

	if e.cmd.Process == nil || e.cmd.ProcessState != nil {
		return errors.New("plugin: sandbox not started")
	}

	done := make(chan error, 1)
	go func() {
		if err := json.NewEncoder(e.in).Encode(map[string]any{"cmd": "stop"}); err != nil {
			done <- errors.Join(err, e.killAndWait())
			return
		}
		done <- e.cmd.Wait()
	}()

	select {
	case err := <-done:
		e.closePipes()
		return err
	case <-ctx.Done():
		_ = e.cmd.Process.Kill()
		_ = e.in.Close()
		err := <-done
		e.closePipes()
		return errors.Join(ctx.Err(), err)
	}
}
killAndWait
Method

Returns

error
func (*ExecSandbox) killAndWait() error
{
	if e.cmd.Process == nil || e.cmd.ProcessState != nil {
		e.closePipes()
		return nil
	}
	killErr := e.cmd.Process.Kill()
	waitErr := e.cmd.Wait()
	e.closePipes()
	return errors.Join(killErr, waitErr)
}
closePipes
Method
func (*ExecSandbox) closePipes()
{
	if e.in != nil {
		_ = e.in.Close()
	}
	if e.out != nil {
		_ = e.out.Close()
	}
}

Fields

Name Type Description
cmd *exec.Cmd
in io.WriteCloser
out io.ReadCloser
mu sync.Mutex
F
function

NewExecSandbox

NewExecSandbox creates a sandbox around the provided binary path and arguments.

Parameters

path
string
args
...string

Returns

core/plugin/sandbox.go:27-29
func NewExecSandbox(path string, args ...string) *ExecSandbox

{
	return &ExecSandbox{cmd: exec.Command(path, args...)}
}
F
function

TestExecSandboxCleansUpAfterStartTimeout

Parameters

core/plugin/sandbox_test.go:10-21
func TestExecSandboxCleansUpAfterStartTimeout(t *testing.T)

{
	sandbox := NewExecSandbox("/bin/sh", "-c", "sleep 30")
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
	defer cancel()

	if err := sandbox.Start(ctx); !errors.Is(err, context.DeadlineExceeded) {
		t.Fatalf("Start() error = %v, want deadline exceeded", err)
	}
	if sandbox.cmd.ProcessState == nil {
		t.Fatal("timed out sandbox process was not reaped")
	}
}
F
function

TestExecSandboxStopContextKillsUnresponsiveProcess

Parameters

core/plugin/sandbox_test.go:23-41
func TestExecSandboxStopContextKillsUnresponsiveProcess(t *testing.T)

{
	sandbox := NewExecSandbox(
		"/bin/sh",
		"-c",
		`printf '{"ready":true}\n'; sleep 30`,
	)
	if err := sandbox.Start(context.Background()); err != nil {
		t.Fatal(err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
	defer cancel()
	if err := sandbox.StopContext(ctx); !errors.Is(err, context.DeadlineExceeded) {
		t.Fatalf("StopContext() error = %v, want deadline exceeded", err)
	}
	if sandbox.cmd.ProcessState == nil {
		t.Fatal("unresponsive sandbox process was not reaped")
	}
}
F
function

TestExecSandboxReapsProcessWhenStopWriteFails

Parameters

core/plugin/sandbox_test.go:43-60
func TestExecSandboxReapsProcessWhenStopWriteFails(t *testing.T)

{
	sandbox := NewExecSandbox(
		"/bin/sh",
		"-c",
		`printf '{"ready":true}\n'; exec 0<&-; sleep 30`,
	)
	if err := sandbox.Start(context.Background()); err != nil {
		t.Fatal(err)
	}
	time.Sleep(20 * time.Millisecond)

	if err := sandbox.Stop(); err == nil {
		t.Fatal("Stop() succeeded after the child closed stdin")
	}
	if sandbox.cmd.ProcessState == nil {
		t.Fatal("sandbox process was not reaped after stop write failure")
	}
}
S
struct

FactoryRegistry

FactoryRegistry stores plugin factories by name.

core/plugin/factory.go:4-6
type FactoryRegistry struct

Methods

Register
Method

Register stores a named factory.

Parameters

name string
factory Factory

Returns

error
func (*FactoryRegistry) Register(name string, factory Factory) error
{
	if _, ok := f.byName[name]; ok {
		return ErrFactoryExists
	}
	f.byName[name] = factory
	return nil
}
Create
Method

Create builds a plugin from a registered factory.

Parameters

name string

Returns

error
func (*FactoryRegistry) Create(name string) (Plugin, error)
{
	factory, ok := f.byName[name]
	if !ok {
		return nil, ErrFactoryNotFound
	}
	return factory(), nil
}

Fields

Name Type Description
byName map[string]Factory
F
function

NewFactoryRegistry

NewFactoryRegistry creates an empty factory registry.

Returns

core/plugin/factory.go:9-11
func NewFactoryRegistry() *FactoryRegistry

{
	return &FactoryRegistry{byName: make(map[string]Factory)}
}