mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 16:16:32 +00:00
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.
This commit is contained in:
@@ -148,6 +148,7 @@ type TLSOptions struct {
|
|||||||
type PluginConfig struct {
|
type PluginConfig struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Addr string `json:"addr"`
|
Addr string `json:"addr"`
|
||||||
|
Command []string `yaml:"command,omitempty" json:"command,omitempty"`
|
||||||
TLS *TLSConfig `yaml:",omitempty" json:"tls,omitempty"`
|
TLS *TLSConfig `yaml:",omitempty" json:"tls,omitempty"`
|
||||||
Timeout time.Duration `yaml:",omitempty" json:"timeout,omitempty"`
|
Timeout time.Duration `yaml:",omitempty" json:"timeout,omitempty"`
|
||||||
Token string `yaml:",omitempty" json:"token,omitempty"`
|
Token string `yaml:",omitempty" json:"token,omitempty"`
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/go-gost/x/config"
|
"github.com/go-gost/x/config"
|
||||||
"github.com/go-gost/x/internal/loader"
|
"github.com/go-gost/x/internal/loader"
|
||||||
"github.com/go-gost/x/internal/plugin"
|
"github.com/go-gost/x/internal/plugin"
|
||||||
|
xplugin "github.com/go-gost/x/plugin"
|
||||||
"github.com/go-gost/x/registry"
|
"github.com/go-gost/x/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,6 +25,9 @@ func ParseAdmission(cfg *config.AdmissionConfig) admission.Admission {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Plugin != nil {
|
if cfg.Plugin != nil {
|
||||||
|
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||||
|
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||||
|
}
|
||||||
var tlsCfg *tls.Config
|
var tlsCfg *tls.Config
|
||||||
if cfg.Plugin.TLS != nil {
|
if cfg.Plugin.TLS != nil {
|
||||||
tlsCfg = &tls.Config{
|
tlsCfg = &tls.Config{
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/go-gost/x/config"
|
"github.com/go-gost/x/config"
|
||||||
"github.com/go-gost/x/internal/loader"
|
"github.com/go-gost/x/internal/loader"
|
||||||
"github.com/go-gost/x/internal/plugin"
|
"github.com/go-gost/x/internal/plugin"
|
||||||
|
xplugin "github.com/go-gost/x/plugin"
|
||||||
"github.com/go-gost/x/registry"
|
"github.com/go-gost/x/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +28,9 @@ func ParseAuther(cfg *config.AutherConfig) auth.Authenticator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Plugin != nil {
|
if cfg.Plugin != nil {
|
||||||
|
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||||
|
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||||
|
}
|
||||||
var tlsCfg *tls.Config
|
var tlsCfg *tls.Config
|
||||||
if cfg.Plugin.TLS != nil {
|
if cfg.Plugin.TLS != nil {
|
||||||
tlsCfg = &tls.Config{
|
tlsCfg = &tls.Config{
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/go-gost/x/config"
|
"github.com/go-gost/x/config"
|
||||||
"github.com/go-gost/x/internal/loader"
|
"github.com/go-gost/x/internal/loader"
|
||||||
"github.com/go-gost/x/internal/plugin"
|
"github.com/go-gost/x/internal/plugin"
|
||||||
|
xplugin "github.com/go-gost/x/plugin"
|
||||||
"github.com/go-gost/x/registry"
|
"github.com/go-gost/x/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,6 +24,9 @@ func ParseBypass(cfg *config.BypassConfig) bypass.Bypass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Plugin != nil {
|
if cfg.Plugin != nil {
|
||||||
|
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||||
|
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||||
|
}
|
||||||
var tlsCfg *tls.Config
|
var tlsCfg *tls.Config
|
||||||
if cfg.Plugin.TLS != nil {
|
if cfg.Plugin.TLS != nil {
|
||||||
tlsCfg = &tls.Config{
|
tlsCfg = &tls.Config{
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ import (
|
|||||||
selector_parser "github.com/go-gost/x/config/parsing/selector"
|
selector_parser "github.com/go-gost/x/config/parsing/selector"
|
||||||
xhop "github.com/go-gost/x/hop"
|
xhop "github.com/go-gost/x/hop"
|
||||||
hop_plugin "github.com/go-gost/x/hop/plugin"
|
hop_plugin "github.com/go-gost/x/hop/plugin"
|
||||||
xs "github.com/go-gost/x/selector"
|
|
||||||
"github.com/go-gost/x/internal/loader"
|
"github.com/go-gost/x/internal/loader"
|
||||||
"github.com/go-gost/x/internal/plugin"
|
"github.com/go-gost/x/internal/plugin"
|
||||||
"github.com/go-gost/x/metadata"
|
"github.com/go-gost/x/metadata"
|
||||||
mdutil "github.com/go-gost/x/metadata/util"
|
mdutil "github.com/go-gost/x/metadata/util"
|
||||||
|
xplugin "github.com/go-gost/x/plugin"
|
||||||
|
xs "github.com/go-gost/x/selector"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseHop converts a HopConfig into a hop.Hop. It resolves plugin backends
|
// ParseHop converts a HopConfig into a hop.Hop. It resolves plugin backends
|
||||||
@@ -32,6 +33,9 @@ func ParseHop(cfg *config.HopConfig, log logger.Logger) (hop.Hop, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Plugin != nil {
|
if cfg.Plugin != nil {
|
||||||
|
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||||
|
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||||
|
}
|
||||||
var tlsCfg *tls.Config
|
var tlsCfg *tls.Config
|
||||||
if cfg.Plugin.TLS != nil {
|
if cfg.Plugin.TLS != nil {
|
||||||
tlsCfg = &tls.Config{
|
tlsCfg = &tls.Config{
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
hosts_plugin "github.com/go-gost/x/hosts/plugin"
|
hosts_plugin "github.com/go-gost/x/hosts/plugin"
|
||||||
"github.com/go-gost/x/internal/loader"
|
"github.com/go-gost/x/internal/loader"
|
||||||
"github.com/go-gost/x/internal/plugin"
|
"github.com/go-gost/x/internal/plugin"
|
||||||
|
xplugin "github.com/go-gost/x/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseHostMapper converts a HostsConfig into a hosts.HostMapper. It resolves
|
// ParseHostMapper converts a HostsConfig into a hosts.HostMapper. It resolves
|
||||||
@@ -23,6 +24,9 @@ func ParseHostMapper(cfg *config.HostsConfig) hosts.HostMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Plugin != nil {
|
if cfg.Plugin != nil {
|
||||||
|
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||||
|
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||||
|
}
|
||||||
var tlsCfg *tls.Config
|
var tlsCfg *tls.Config
|
||||||
if cfg.Plugin.TLS != nil {
|
if cfg.Plugin.TLS != nil {
|
||||||
tlsCfg = &tls.Config{
|
tlsCfg = &tls.Config{
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
ingress_plugin "github.com/go-gost/x/ingress/plugin"
|
ingress_plugin "github.com/go-gost/x/ingress/plugin"
|
||||||
"github.com/go-gost/x/internal/loader"
|
"github.com/go-gost/x/internal/loader"
|
||||||
"github.com/go-gost/x/internal/plugin"
|
"github.com/go-gost/x/internal/plugin"
|
||||||
|
xplugin "github.com/go-gost/x/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseIngress converts an IngressConfig into an ingress.Ingress. It resolves
|
// ParseIngress converts an IngressConfig into an ingress.Ingress. It resolves
|
||||||
@@ -22,6 +23,9 @@ func ParseIngress(cfg *config.IngressConfig) ingress.Ingress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Plugin != nil {
|
if cfg.Plugin != nil {
|
||||||
|
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||||
|
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||||
|
}
|
||||||
var tlsCfg *tls.Config
|
var tlsCfg *tls.Config
|
||||||
if cfg.Plugin.TLS != nil {
|
if cfg.Plugin.TLS != nil {
|
||||||
tlsCfg = &tls.Config{
|
tlsCfg = &tls.Config{
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
xrate "github.com/go-gost/x/limiter/rate"
|
xrate "github.com/go-gost/x/limiter/rate"
|
||||||
xtraffic "github.com/go-gost/x/limiter/traffic"
|
xtraffic "github.com/go-gost/x/limiter/traffic"
|
||||||
traffic_plugin "github.com/go-gost/x/limiter/traffic/plugin"
|
traffic_plugin "github.com/go-gost/x/limiter/traffic/plugin"
|
||||||
|
xplugin "github.com/go-gost/x/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseTrafficLimiter converts a LimiterConfig into a traffic.TrafficLimiter.
|
// ParseTrafficLimiter converts a LimiterConfig into a traffic.TrafficLimiter.
|
||||||
@@ -26,6 +27,9 @@ func ParseTrafficLimiter(cfg *config.LimiterConfig) (lim traffic.TrafficLimiter)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Plugin != nil {
|
if cfg.Plugin != nil {
|
||||||
|
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||||
|
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||||
|
}
|
||||||
var tlsCfg *tls.Config
|
var tlsCfg *tls.Config
|
||||||
if cfg.Plugin.TLS != nil {
|
if cfg.Plugin.TLS != nil {
|
||||||
tlsCfg = &tls.Config{
|
tlsCfg = &tls.Config{
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/logger"
|
||||||
"github.com/go-gost/core/observer"
|
"github.com/go-gost/core/observer"
|
||||||
"github.com/go-gost/x/config"
|
"github.com/go-gost/x/config"
|
||||||
"github.com/go-gost/x/internal/plugin"
|
"github.com/go-gost/x/internal/plugin"
|
||||||
observer_plugin "github.com/go-gost/x/observer/plugin"
|
observer_plugin "github.com/go-gost/x/observer/plugin"
|
||||||
|
xplugin "github.com/go-gost/x/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseObserver converts an ObserverConfig into an observer.Observer. It only
|
// ParseObserver converts an ObserverConfig into an observer.Observer. It only
|
||||||
@@ -18,6 +20,9 @@ func ParseObserver(cfg *config.ObserverConfig) observer.Observer {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||||
|
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||||
|
}
|
||||||
var tlsCfg *tls.Config
|
var tlsCfg *tls.Config
|
||||||
if cfg.Plugin.TLS != nil {
|
if cfg.Plugin.TLS != nil {
|
||||||
tlsCfg = &tls.Config{
|
tlsCfg = &tls.Config{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/go-gost/x/internal/plugin"
|
"github.com/go-gost/x/internal/plugin"
|
||||||
xp2p "github.com/go-gost/x/p2p"
|
xp2p "github.com/go-gost/x/p2p"
|
||||||
p2p_plugin "github.com/go-gost/x/p2p/plugin"
|
p2p_plugin "github.com/go-gost/x/p2p/plugin"
|
||||||
|
xplugin "github.com/go-gost/x/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseP2P converts a P2PConfig into a p2p.TunnelProvider backed by an
|
// ParseP2P converts a P2PConfig into a p2p.TunnelProvider backed by an
|
||||||
@@ -21,6 +22,9 @@ func ParseP2P(cfg *config.P2PConfig) (p xp2p.TunnelProvider) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||||
|
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||||
|
}
|
||||||
var tlsCfg *tls.Config
|
var tlsCfg *tls.Config
|
||||||
if cfg.Plugin.TLS != nil {
|
if cfg.Plugin.TLS != nil {
|
||||||
tlsCfg = &tls.Config{
|
tlsCfg = &tls.Config{
|
||||||
@@ -40,4 +44,4 @@ func ParseP2P(cfg *config.P2PConfig) (p xp2p.TunnelProvider) {
|
|||||||
plugin.TLSConfigOption(tlsCfg),
|
plugin.TLSConfigOption(tlsCfg),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/go-gost/core/recorder"
|
"github.com/go-gost/core/recorder"
|
||||||
"github.com/go-gost/x/config"
|
"github.com/go-gost/x/config"
|
||||||
"github.com/go-gost/x/internal/plugin"
|
"github.com/go-gost/x/internal/plugin"
|
||||||
|
xplugin "github.com/go-gost/x/plugin"
|
||||||
xrecorder "github.com/go-gost/x/recorder"
|
xrecorder "github.com/go-gost/x/recorder"
|
||||||
recorder_plugin "github.com/go-gost/x/recorder/plugin"
|
recorder_plugin "github.com/go-gost/x/recorder/plugin"
|
||||||
"gopkg.in/natefinch/lumberjack.v2"
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
@@ -33,6 +34,9 @@ func ParseRecorder(cfg *config.RecorderConfig) (r recorder.Recorder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Plugin != nil {
|
if cfg.Plugin != nil {
|
||||||
|
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||||
|
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||||
|
}
|
||||||
var tlsCfg *tls.Config
|
var tlsCfg *tls.Config
|
||||||
if cfg.Plugin.TLS != nil {
|
if cfg.Plugin.TLS != nil {
|
||||||
tlsCfg = &tls.Config{
|
tlsCfg = &tls.Config{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/go-gost/core/resolver"
|
"github.com/go-gost/core/resolver"
|
||||||
"github.com/go-gost/x/config"
|
"github.com/go-gost/x/config"
|
||||||
"github.com/go-gost/x/internal/plugin"
|
"github.com/go-gost/x/internal/plugin"
|
||||||
|
xplugin "github.com/go-gost/x/plugin"
|
||||||
"github.com/go-gost/x/registry"
|
"github.com/go-gost/x/registry"
|
||||||
xresolver "github.com/go-gost/x/resolver"
|
xresolver "github.com/go-gost/x/resolver"
|
||||||
resolver_plugin "github.com/go-gost/x/resolver/plugin"
|
resolver_plugin "github.com/go-gost/x/resolver/plugin"
|
||||||
@@ -24,6 +25,9 @@ func ParseResolver(cfg *config.ResolverConfig) (resolver.Resolver, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Plugin != nil {
|
if cfg.Plugin != nil {
|
||||||
|
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||||
|
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||||
|
}
|
||||||
var tlsCfg *tls.Config
|
var tlsCfg *tls.Config
|
||||||
if cfg.Plugin.TLS != nil {
|
if cfg.Plugin.TLS != nil {
|
||||||
tlsCfg = &tls.Config{
|
tlsCfg = &tls.Config{
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/logger"
|
||||||
"github.com/go-gost/core/rewriter"
|
"github.com/go-gost/core/rewriter"
|
||||||
"github.com/go-gost/x/config"
|
"github.com/go-gost/x/config"
|
||||||
"github.com/go-gost/x/internal/plugin"
|
"github.com/go-gost/x/internal/plugin"
|
||||||
|
xplugin "github.com/go-gost/x/plugin"
|
||||||
rewriter_plugin "github.com/go-gost/x/rewriter/plugin"
|
rewriter_plugin "github.com/go-gost/x/rewriter/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +21,9 @@ func ParseRewriter(cfg *config.RewriterConfig) rewriter.Rewriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Plugin != nil {
|
if cfg.Plugin != nil {
|
||||||
|
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||||
|
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||||
|
}
|
||||||
var tlsCfg *tls.Config
|
var tlsCfg *tls.Config
|
||||||
if cfg.Plugin.TLS != nil {
|
if cfg.Plugin.TLS != nil {
|
||||||
tlsCfg = &tls.Config{
|
tlsCfg = &tls.Config{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/go-gost/x/config"
|
"github.com/go-gost/x/config"
|
||||||
"github.com/go-gost/x/internal/loader"
|
"github.com/go-gost/x/internal/loader"
|
||||||
"github.com/go-gost/x/internal/plugin"
|
"github.com/go-gost/x/internal/plugin"
|
||||||
|
xplugin "github.com/go-gost/x/plugin"
|
||||||
xrouter "github.com/go-gost/x/router"
|
xrouter "github.com/go-gost/x/router"
|
||||||
router_plugin "github.com/go-gost/x/router/plugin"
|
router_plugin "github.com/go-gost/x/router/plugin"
|
||||||
)
|
)
|
||||||
@@ -23,6 +24,9 @@ func ParseRouter(cfg *config.RouterConfig) router.Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Plugin != nil {
|
if cfg.Plugin != nil {
|
||||||
|
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||||
|
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||||
|
}
|
||||||
var tlsCfg *tls.Config
|
var tlsCfg *tls.Config
|
||||||
if cfg.Plugin.TLS != nil {
|
if cfg.Plugin.TLS != nil {
|
||||||
tlsCfg = &tls.Config{
|
tlsCfg = &tls.Config{
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/logger"
|
||||||
"github.com/go-gost/core/sd"
|
"github.com/go-gost/core/sd"
|
||||||
"github.com/go-gost/x/config"
|
"github.com/go-gost/x/config"
|
||||||
"github.com/go-gost/x/internal/plugin"
|
"github.com/go-gost/x/internal/plugin"
|
||||||
|
xplugin "github.com/go-gost/x/plugin"
|
||||||
sd_plugin "github.com/go-gost/x/sd/plugin"
|
sd_plugin "github.com/go-gost/x/sd/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,6 +19,9 @@ func ParseSD(cfg *config.SDConfig) sd.SD {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||||
|
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||||
|
}
|
||||||
var tlsCfg *tls.Config
|
var tlsCfg *tls.Config
|
||||||
if cfg.Plugin.TLS != nil {
|
if cfg.Plugin.TLS != nil {
|
||||||
tlsCfg = &tls.Config{
|
tlsCfg = &tls.Config{
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// 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()
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// prepare arranges for the child to receive SIGTERM when the parent process
|
||||||
|
// dies, including an abrupt exit (panic, kill -9). It uses prctl(PR_SET_PDEATHSIG).
|
||||||
|
func prepare(cmd *exec.Cmd) {
|
||||||
|
if cmd.SysProcAttr == nil {
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||||
|
}
|
||||||
|
cmd.SysProcAttr.Pdeathsig = syscall.SIGTERM
|
||||||
|
}
|
||||||
|
|
||||||
|
func attach(cmd *exec.Cmd) {}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build !linux && !windows
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import "os/exec"
|
||||||
|
|
||||||
|
// prepare and attach are no-ops on platforms without a parent-side mechanism
|
||||||
|
// to terminate the child on parent death. Children spawned here will not
|
||||||
|
// follow an abrupt parent exit (panic / kill -9).
|
||||||
|
func prepare(cmd *exec.Cmd) {}
|
||||||
|
|
||||||
|
func attach(cmd *exec.Cmd) {}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/logger"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
jobOnce sync.Once
|
||||||
|
job windows.Handle
|
||||||
|
jobErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
func initJob() {
|
||||||
|
handle, err := windows.CreateJobObject(nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
jobErr = err
|
||||||
|
logger.Default().Errorf("plugin: create job object: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{
|
||||||
|
BasicLimitInformation: windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{
|
||||||
|
LimitFlags: windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if _, err := windows.SetInformationJobObject(
|
||||||
|
handle,
|
||||||
|
windows.JobObjectExtendedLimitInformation,
|
||||||
|
uintptr(unsafe.Pointer(&info)),
|
||||||
|
uint32(unsafe.Sizeof(info)),
|
||||||
|
); err != nil {
|
||||||
|
jobErr = err
|
||||||
|
windows.CloseHandle(handle)
|
||||||
|
logger.Default().Errorf("plugin: configure job object: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
job = handle
|
||||||
|
}
|
||||||
|
|
||||||
|
// processAllAccess is PROCESS_ALL_ACCESS, needed to open the child for
|
||||||
|
// AssignProcessToJobObject.
|
||||||
|
const processAllAccess = windows.STANDARD_RIGHTS_REQUIRED | windows.SYNCHRONIZE | 0xffff
|
||||||
|
|
||||||
|
func prepare(cmd *exec.Cmd) {}
|
||||||
|
|
||||||
|
// attach assigns the started child to the shared job object whose
|
||||||
|
// KILL_ON_JOB_CLOSE flag makes the OS terminate the child when the parent
|
||||||
|
// process dies (even abruptly). Failure (e.g. the parent is already in a job)
|
||||||
|
// is non-fatal: the child simply loses the die-with-parent guarantee.
|
||||||
|
func attach(cmd *exec.Cmd) {
|
||||||
|
if cmd.Process == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobOnce.Do(initJob)
|
||||||
|
if jobErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handle, err := windows.OpenProcess(processAllAccess, false, uint32(cmd.Process.Pid))
|
||||||
|
if err != nil {
|
||||||
|
logger.Default().Errorf("plugin: open child process: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer windows.CloseHandle(handle)
|
||||||
|
|
||||||
|
if err := windows.AssignProcessToJobObject(job, handle); err != nil {
|
||||||
|
logger.Default().Errorf("plugin: assign child to job object: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user