Plugin lifecycle optimization

- Added term flow before plugin is killed
- Updated example implementations
- Added SIGINT to Zoraxy for shutdown sequence (Fixes #561 ?)
This commit is contained in:
Toby Chui
2025-03-01 10:00:33 +08:00
parent 14e1341c34
commit 28a0a837ba
16 changed files with 255 additions and 139 deletions

View File

@@ -4,12 +4,14 @@ import (
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
@@ -146,20 +148,55 @@ func (m *Manager) StopPlugin(pluginID string) error {
}
thisPlugin := plugin.(*Plugin)
thisPlugin.process.Process.Signal(os.Interrupt)
go func() {
//Wait for 10 seconds for the plugin to stop gracefully
time.Sleep(10 * time.Second)
var err error
//Make a GET request to plugin ui path /term to gracefully stop the plugin
if thisPlugin.uiProxy != nil {
requestURI := "http://127.0.0.1:" + strconv.Itoa(thisPlugin.AssignedPort) + "/" + thisPlugin.Spec.UIPath + "/term"
resp, err := http.Get(requestURI)
if err != nil {
//Plugin do not support termination request, do it the hard way
m.Log("Plugin "+thisPlugin.Spec.ID+" termination request failed. Force shutting down", nil)
} else {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusNotFound {
m.Log("Plugin "+thisPlugin.Spec.ID+" does not support termination request", nil)
} else {
m.Log("Plugin "+thisPlugin.Spec.ID+" termination request returned status: "+resp.Status, nil)
}
}
}
}
if runtime.GOOS == "windows" && thisPlugin.process != nil {
//There is no SIGTERM in windows, kill the process directly
time.Sleep(300 * time.Millisecond)
thisPlugin.process.Process.Kill()
} else {
//Send SIGTERM to the plugin process, if it is still running
err = thisPlugin.process.Process.Signal(syscall.SIGTERM)
if err != nil {
m.Log("Failed to send Interrupt signal to plugin "+thisPlugin.Spec.Name+": "+err.Error(), nil)
}
//Wait for the plugin to stop
for range 5 {
time.Sleep(1 * time.Second)
if thisPlugin.process.ProcessState != nil && thisPlugin.process.ProcessState.Exited() {
m.Log("Plugin "+thisPlugin.Spec.Name+" background process stopped", nil)
break
}
}
if thisPlugin.process.ProcessState == nil || !thisPlugin.process.ProcessState.Exited() {
m.Log("Plugin "+thisPlugin.Spec.Name+" failed to stop gracefully, killing it", nil)
thisPlugin.process.Process.Kill()
} else {
m.Log("Plugin "+thisPlugin.Spec.Name+" background process stopped", nil)
}
}
//Remove the UI proxy
thisPlugin.uiProxy = nil
}()
//Remove the UI proxy
thisPlugin.uiProxy = nil
plugin.(*Plugin).Enabled = false
return nil
}

View File

@@ -112,7 +112,7 @@ func (m *Manager) GetPluginPreviousEnableState(pluginID string) bool {
// ListLoadedPlugins returns a list of loaded plugins
func (m *Manager) ListLoadedPlugins() ([]*Plugin, error) {
var plugins []*Plugin
var plugins []*Plugin = []*Plugin{}
m.LoadedPlugins.Range(func(key, value interface{}) bool {
plugin := value.(*Plugin)
plugins = append(plugins, plugin)

View File

@@ -6,6 +6,7 @@ import (
"io/fs"
"net/http"
"net/url"
"os"
"strings"
"time"
)
@@ -15,6 +16,8 @@ type PluginUiRouter struct {
TargetFs *embed.FS //The embed.FS where the UI files are stored
TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
terminateHandler func() //The handler to be called when the plugin is terminated
}
// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS
@@ -104,3 +107,22 @@ func (p *PluginUiRouter) Handler() http.Handler {
p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r)
})
}
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
// if mux is nil, the handler will be registered to http.DefaultServeMux
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
p.terminateHandler = termFunc
if mux == nil {
mux = http.DefaultServeMux
}
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
p.terminateHandler()
w.WriteHeader(http.StatusOK)
go func() {
//Make sure the response is sent before the plugin is terminated
time.Sleep(100 * time.Millisecond)
os.Exit(0)
}()
})
}

View File

@@ -4,9 +4,7 @@ import (
"encoding/json"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
)
/*
@@ -174,25 +172,3 @@ func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
ServeIntroSpect(pluginSpect)
return RecvConfigureSpec()
}
/*
Shutdown handler
This function will register a shutdown handler for the plugin
The shutdown callback will be called when the plugin is shutting down
You can use this to clean up resources like closing database connections
*/
func RegisterShutdownHandler(shutdownCallback func()) {
// Set up a channel to receive OS signals
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Start a goroutine to listen for signals
go func() {
<-sigChan
shutdownCallback()
os.Exit(0)
}()
}