Files
gost-x/plugin/process.go
T
ginuerzh f70b114578 plugin: manage external plugin subprocess lifecycle
Add Command []string to PluginConfig and a new x/plugin package that
spawns plugin subprocesses (deduplicated by argv) and terminates them
on Shutdown. Linux uses Pdeathsig, Windows a kill-on-close job object,
so children follow an abrupt parent exit.

Wire Spawn into every config/parsing plugin constructor.
2026-09-07 17:40:20 +08:00

78 lines
1.8 KiB
Go

// Package plugin manages the lifecycle of external plugin subprocesses
// spawned from plugin configuration.
//
// Plugins are normally external processes that GOST connects to over HTTP or
// gRPC. When a plugin config carries a non-empty Command, Spawn starts it as a
// child process of GOST so that a single invocation brings up the whole stack,
// and Shutdown terminates it again.
package plugin
import (
"context"
"os/exec"
"strings"
"sync"
)
var (
mu sync.Mutex
children = map[string]*exec.Cmd{}
ctx context.Context
cancel context.CancelFunc
)
// Spawn launches command as a child process, deduplicated by its argv string.
// An empty command is a no-op. A previously-started command that is still
// running is reused. It returns an error only when the process fails to start.
func Spawn(command []string) error {
if len(command) == 0 {
return nil
}
key := strings.Join(command, "\x00")
mu.Lock()
if _, ok := children[key]; ok {
// ponytail: no liveness probe; a crashed child is reaped by the Wait
// goroutine and respawned on the next reload.
mu.Unlock()
return nil
}
if ctx == nil {
ctx, cancel = context.WithCancel(context.Background())
}
cmd := exec.CommandContext(ctx, command[0], command[1:]...)
// Stdout/Stderr are left nil so the child's output goes to the null
// device; the plugin owns its own log storage.
prepare(cmd)
if err := cmd.Start(); err != nil {
mu.Unlock()
return err
}
attach(cmd)
children[key] = cmd
mu.Unlock()
go func(key string, cmd *exec.Cmd) {
cmd.Wait()
mu.Lock()
delete(children, key)
mu.Unlock()
}(key, cmd)
return nil
}
// Shutdown terminates all spawned plugin subprocesses.
func Shutdown() {
if cancel != nil {
cancel()
}
mu.Lock()
children = map[string]*exec.Cmd{}
mu.Unlock()
}