mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-03 06:07:20 +02:00
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:
parent
14e1341c34
commit
28a0a837ba
@ -43,16 +43,21 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the shutdown handler
|
// Create a new PluginEmbedUIRouter that will serve the UI from web folder
|
||||||
plugin.RegisterShutdownHandler(func() {
|
// The router will also help to handle the termination of the plugin when
|
||||||
|
// a user wants to stop the plugin via Zoraxy Web UI
|
||||||
|
embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
|
||||||
|
embedWebRouter.RegisterTerminateHandler(func() {
|
||||||
// Do cleanup here if needed
|
// Do cleanup here if needed
|
||||||
fmt.Println("Hello World Plugin Exited")
|
fmt.Println("Hello World Plugin Exited")
|
||||||
})
|
}, nil)
|
||||||
|
|
||||||
embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
|
|
||||||
|
|
||||||
// Serve the hello world page in the www folder
|
// Serve the hello world page in the www folder
|
||||||
http.Handle(UI_PATH, embedWebRouter.Handler())
|
http.Handle(UI_PATH, embedWebRouter.Handler())
|
||||||
fmt.Println("Hello World started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
|
fmt.Println("Hello World started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
|
||||||
http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
|
err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -15,6 +16,8 @@ type PluginUiRouter struct {
|
|||||||
TargetFs *embed.FS //The embed.FS where the UI files are stored
|
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
|
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
|
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
|
// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS
|
||||||
@ -91,6 +94,7 @@ func (p *PluginUiRouter) Handler() http.Handler {
|
|||||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||||
r.URL, _ = url.Parse(rewrittenURL)
|
r.URL, _ = url.Parse(rewrittenURL)
|
||||||
r.RequestURI = rewrittenURL
|
r.RequestURI = rewrittenURL
|
||||||
|
|
||||||
//Serve the file from the embed.FS
|
//Serve the file from the embed.FS
|
||||||
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
|
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -103,3 +107,22 @@ func (p *PluginUiRouter) Handler() http.Handler {
|
|||||||
p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r)
|
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)
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -4,9 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -79,9 +77,8 @@ type IntroSpect struct {
|
|||||||
Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on
|
Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on
|
||||||
This captures the whole traffic of Zoraxy
|
This captures the whole traffic of Zoraxy
|
||||||
|
|
||||||
Notes: Will raise a warning on the UI when the user enables the plugin on a HTTP Proxy rule
|
|
||||||
*/
|
*/
|
||||||
GlobalCapturePath []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin
|
GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin
|
||||||
GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler)
|
GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -90,20 +87,9 @@ type IntroSpect struct {
|
|||||||
Once the plugin is enabled on a given HTTP Proxy rule,
|
Once the plugin is enabled on a given HTTP Proxy rule,
|
||||||
these always applies
|
these always applies
|
||||||
*/
|
*/
|
||||||
AlwaysCapturePath []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp)
|
AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp)
|
||||||
AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler)
|
AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler)
|
||||||
|
|
||||||
/*
|
|
||||||
Dynamic Capture Settings
|
|
||||||
|
|
||||||
Once the plugin is enabled on a given HTTP Proxy rule,
|
|
||||||
the plugin can capture the request and decided if the request
|
|
||||||
shall be handled by itself or let it pass through
|
|
||||||
|
|
||||||
*/
|
|
||||||
DynmaicCaptureIngress string `json:"capture_path"` //Traffic capture path of your plugin (e.g. /capture)
|
|
||||||
DynamicHandleIngress string `json:"handle_path"` //Traffic handle path of your plugin (e.g. /handler)
|
|
||||||
|
|
||||||
/* UI Path for your plugin */
|
/* UI Path for your plugin */
|
||||||
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
||||||
|
|
||||||
@ -186,25 +172,3 @@ func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
|
|||||||
ServeIntroSpect(pluginSpect)
|
ServeIntroSpect(pluginSpect)
|
||||||
return RecvConfigureSpec()
|
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)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
@ -51,17 +51,17 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new PluginEmbedUIRouter that will serve the UI from web folder
|
||||||
|
uiRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, EMBED_FS_ROOT, UI_RELPATH)
|
||||||
|
|
||||||
// Register the shutdown handler
|
// Register the shutdown handler
|
||||||
plugin.RegisterShutdownHandler(func() {
|
uiRouter.RegisterTerminateHandler(func() {
|
||||||
fmt.Println("Shutting down ZeroTier Network Controller")
|
// Do cleanup here if needed
|
||||||
if sysdb != nil {
|
if sysdb != nil {
|
||||||
sysdb.Close()
|
sysdb.Close()
|
||||||
}
|
}
|
||||||
fmt.Println("ZeroTier Network Controller Exited")
|
fmt.Println("ztnc Exited")
|
||||||
})
|
}, nil)
|
||||||
|
|
||||||
// Create a new PluginEmbedUIRouter that will serve the UI from web folder
|
|
||||||
uiRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, EMBED_FS_ROOT, UI_RELPATH)
|
|
||||||
|
|
||||||
// This will serve the index.html file embedded in the binary
|
// This will serve the index.html file embedded in the binary
|
||||||
http.Handle(UI_RELPATH+"/", uiRouter.Handler())
|
http.Handle(UI_RELPATH+"/", uiRouter.Handler())
|
||||||
|
19
example/plugins/ztnc/mod/zoraxy_plugin/README.txt
Normal file
19
example/plugins/ztnc/mod/zoraxy_plugin/README.txt
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Zoraxy Plugin
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. **Copy the Module:**
|
||||||
|
- Copy the entire `zoraxy_plugin` module to your plugin mod folder.
|
||||||
|
|
||||||
|
2. **Include the Structure:**
|
||||||
|
- Ensure that you maintain the directory structure and file organization as provided in this module.
|
||||||
|
|
||||||
|
3. **Modify as Needed:**
|
||||||
|
- Customize the copied module to implement the desired functionality for your plugin.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup
|
||||||
|
embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages
|
@ -6,6 +6,7 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -15,6 +16,8 @@ type PluginUiRouter struct {
|
|||||||
TargetFs *embed.FS //The embed.FS where the UI files are stored
|
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
|
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
|
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
|
// 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)
|
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)
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -4,9 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -79,9 +77,8 @@ type IntroSpect struct {
|
|||||||
Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on
|
Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on
|
||||||
This captures the whole traffic of Zoraxy
|
This captures the whole traffic of Zoraxy
|
||||||
|
|
||||||
Notes: Will raise a warning on the UI when the user enables the plugin on a HTTP Proxy rule
|
|
||||||
*/
|
*/
|
||||||
GlobalCapturePath []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin
|
GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin
|
||||||
GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler)
|
GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -90,20 +87,9 @@ type IntroSpect struct {
|
|||||||
Once the plugin is enabled on a given HTTP Proxy rule,
|
Once the plugin is enabled on a given HTTP Proxy rule,
|
||||||
these always applies
|
these always applies
|
||||||
*/
|
*/
|
||||||
AlwaysCapturePath []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp)
|
AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp)
|
||||||
AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler)
|
AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler)
|
||||||
|
|
||||||
/*
|
|
||||||
Dynamic Capture Settings
|
|
||||||
|
|
||||||
Once the plugin is enabled on a given HTTP Proxy rule,
|
|
||||||
the plugin can capture the request and decided if the request
|
|
||||||
shall be handled by itself or let it pass through
|
|
||||||
|
|
||||||
*/
|
|
||||||
DynmaicCaptureIngress string `json:"capture_path"` //Traffic capture path of your plugin (e.g. /capture)
|
|
||||||
DynamicHandleIngress string `json:"handle_path"` //Traffic handle path of your plugin (e.g. /handler)
|
|
||||||
|
|
||||||
/* UI Path for your plugin */
|
/* UI Path for your plugin */
|
||||||
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
||||||
|
|
||||||
@ -186,25 +172,3 @@ func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
|
|||||||
ServeIntroSpect(pluginSpect)
|
ServeIntroSpect(pluginSpect)
|
||||||
return RecvConfigureSpec()
|
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)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
@ -44,7 +44,7 @@ const (
|
|||||||
/* Build Constants */
|
/* Build Constants */
|
||||||
SYSTEM_NAME = "Zoraxy"
|
SYSTEM_NAME = "Zoraxy"
|
||||||
SYSTEM_VERSION = "3.1.9"
|
SYSTEM_VERSION = "3.1.9"
|
||||||
DEVELOPMENT_BUILD = true /* Development: Set to false to use embedded web fs */
|
DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */
|
||||||
|
|
||||||
/* System Constants */
|
/* System Constants */
|
||||||
TMP_FOLDER = "./tmp"
|
TMP_FOLDER = "./tmp"
|
||||||
|
@ -50,7 +50,7 @@ import (
|
|||||||
/* SIGTERM handler, do shutdown sequences before closing */
|
/* SIGTERM handler, do shutdown sequences before closing */
|
||||||
func SetupCloseHandler() {
|
func SetupCloseHandler() {
|
||||||
c := make(chan os.Signal, 2)
|
c := make(chan os.Signal, 2)
|
||||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
|
||||||
go func() {
|
go func() {
|
||||||
<-c
|
<-c
|
||||||
ShutdownSeq()
|
ShutdownSeq()
|
||||||
|
@ -4,12 +4,14 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||||
@ -146,20 +148,55 @@ func (m *Manager) StopPlugin(pluginID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
thisPlugin := plugin.(*Plugin)
|
thisPlugin := plugin.(*Plugin)
|
||||||
thisPlugin.process.Process.Signal(os.Interrupt)
|
var err error
|
||||||
go func() {
|
|
||||||
//Wait for 10 seconds for the plugin to stop gracefully
|
//Make a GET request to plugin ui path /term to gracefully stop the plugin
|
||||||
time.Sleep(10 * time.Second)
|
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() {
|
if thisPlugin.process.ProcessState == nil || !thisPlugin.process.ProcessState.Exited() {
|
||||||
m.Log("Plugin "+thisPlugin.Spec.Name+" failed to stop gracefully, killing it", nil)
|
m.Log("Plugin "+thisPlugin.Spec.Name+" failed to stop gracefully, killing it", nil)
|
||||||
thisPlugin.process.Process.Kill()
|
thisPlugin.process.Process.Kill()
|
||||||
} else {
|
|
||||||
m.Log("Plugin "+thisPlugin.Spec.Name+" background process stopped", nil)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Remove the UI proxy
|
//Remove the UI proxy
|
||||||
thisPlugin.uiProxy = nil
|
thisPlugin.uiProxy = nil
|
||||||
}()
|
|
||||||
plugin.(*Plugin).Enabled = false
|
plugin.(*Plugin).Enabled = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@ func (m *Manager) GetPluginPreviousEnableState(pluginID string) bool {
|
|||||||
|
|
||||||
// ListLoadedPlugins returns a list of loaded plugins
|
// ListLoadedPlugins returns a list of loaded plugins
|
||||||
func (m *Manager) ListLoadedPlugins() ([]*Plugin, error) {
|
func (m *Manager) ListLoadedPlugins() ([]*Plugin, error) {
|
||||||
var plugins []*Plugin
|
var plugins []*Plugin = []*Plugin{}
|
||||||
m.LoadedPlugins.Range(func(key, value interface{}) bool {
|
m.LoadedPlugins.Range(func(key, value interface{}) bool {
|
||||||
plugin := value.(*Plugin)
|
plugin := value.(*Plugin)
|
||||||
plugins = append(plugins, plugin)
|
plugins = append(plugins, plugin)
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -15,6 +16,8 @@ type PluginUiRouter struct {
|
|||||||
TargetFs *embed.FS //The embed.FS where the UI files are stored
|
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
|
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
|
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
|
// 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)
|
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)
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -4,9 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -174,25 +172,3 @@ func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
|
|||||||
ServeIntroSpect(pluginSpect)
|
ServeIntroSpect(pluginSpect)
|
||||||
return RecvConfigureSpec()
|
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)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
@ -4,13 +4,23 @@
|
|||||||
</iframe>
|
</iframe>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
function initPluginUIView(){
|
function initPluginUIView(forceOverwritePluginID = undefined){
|
||||||
|
if (typeof(forceOverwritePluginID) != "undefined"){
|
||||||
|
let pluginID = forceOverwritePluginID;
|
||||||
|
console.log("Launching plugin UI for plugin with ID:", pluginID);
|
||||||
|
loadPluginContext(pluginID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
let pluginID = getPluginIDFromWindowHash();
|
let pluginID = getPluginIDFromWindowHash();
|
||||||
|
if (pluginID == ""){
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log("Launching plugin UI for plugin with ID:", pluginID);
|
console.log("Launching plugin UI for plugin with ID:", pluginID);
|
||||||
loadPluginContext(pluginID);
|
loadPluginContext(pluginID);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPluginContext(pluginID){
|
function loadPluginContext(pluginID){
|
||||||
|
//Check if the iframe is currently visable
|
||||||
let pluginContextURL = `/plugin.ui/${pluginID}/`;
|
let pluginContextURL = `/plugin.ui/${pluginID}/`;
|
||||||
$("#pluginContextLoader").attr("src", pluginContextURL);
|
$("#pluginContextLoader").attr("src", pluginContextURL);
|
||||||
}
|
}
|
||||||
|
@ -24,16 +24,37 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
function initPluginSideMenu(){
|
function initPluginSideMenu(){
|
||||||
$("#pluginMenu").html("");
|
|
||||||
$.get(`/api/plugins/list`, function(data){
|
$.get(`/api/plugins/list`, function(data){
|
||||||
|
$("#pluginMenu").html("");
|
||||||
|
let enabledPluginCount = 0;
|
||||||
data.forEach(plugin => {
|
data.forEach(plugin => {
|
||||||
|
if (!plugin.Enabled){
|
||||||
|
return;
|
||||||
|
}
|
||||||
$("#pluginMenu").append(`
|
$("#pluginMenu").append(`
|
||||||
<a class="item" tag="pluginContextWindow" pluginid="${plugin.Spec.id}" onclick="loadPluginUIContextIfAvailable();">
|
<a class="item" tag="pluginContextWindow" pluginid="${plugin.Spec.id}">
|
||||||
<img style="width: 20px;" class="ui mini right spaced image" src="/api/plugins/icon?plugin_id=${plugin.Spec.id}"> ${plugin.Spec.name}
|
<img style="width: 20px;" class="ui mini right spaced image" src="/api/plugins/icon?plugin_id=${plugin.Spec.id}"> ${plugin.Spec.name}
|
||||||
</a>
|
</a>
|
||||||
`);
|
`);
|
||||||
|
enabledPluginCount++;
|
||||||
});
|
});
|
||||||
|
if (enabledPluginCount == 0){
|
||||||
|
$("#pluginMenu").append(`
|
||||||
|
<a class="item" style="pointer-events: none; user-select: none; opacity: 0.6;">
|
||||||
|
<i class="green circle check icon"></i> No Plugins Installed
|
||||||
|
</a>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Rebind events for the plugin menu
|
||||||
|
$("#pluginMenu").find(".item").each(function(){
|
||||||
|
$(this).off("click").on("click", function(event){
|
||||||
|
let tabid = $(this).attr("tag");
|
||||||
|
openTabById(tabid, $(this));
|
||||||
|
loadPluginUIContextIfAvailable();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
initPluginSideMenu();
|
initPluginSideMenu();
|
||||||
@ -44,12 +65,9 @@ function loadPluginUIContextIfAvailable(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function initiatePluginList(){
|
function initiatePluginList(){
|
||||||
$.get(`/api/plugins/list`, function(data){
|
$.get(`/api/plugins/list`, function(data){
|
||||||
const tbody = $("#pluginTable");
|
$("#pluginTable").html("");
|
||||||
tbody.empty();
|
|
||||||
|
|
||||||
data.forEach(plugin => {
|
data.forEach(plugin => {
|
||||||
let authorContact = plugin.Spec.author_contact;
|
let authorContact = plugin.Spec.author_contact;
|
||||||
@ -62,7 +80,7 @@ function initiatePluginList(){
|
|||||||
<tr>
|
<tr>
|
||||||
<td data-label="PluginName">
|
<td data-label="PluginName">
|
||||||
<h4 class="ui header">
|
<h4 class="ui header">
|
||||||
<img onclick="openPluginUI('${plugin.Spec.id}');" class="clickable" src="/api/plugins/icon?plugin_id=${plugin.Spec.id}" class="ui image">
|
<img src="/api/plugins/icon?plugin_id=${plugin.Spec.id}" class="ui image">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
${plugin.Spec.name}
|
${plugin.Spec.name}
|
||||||
<div class="sub header">${versionString} by <a href="${authorContact}" target="_blank">${plugin.Spec.author}</a></div>
|
<div class="sub header">${versionString} by <a href="${authorContact}" target="_blank">${plugin.Spec.author}</a></div>
|
||||||
@ -73,20 +91,76 @@ function initiatePluginList(){
|
|||||||
<a href="${plugin.Spec.url}" target="_blank">${plugin.Spec.url}</a></td>
|
<a href="${plugin.Spec.url}" target="_blank">${plugin.Spec.url}</a></td>
|
||||||
<td data-label="Category">${plugin.Spec.type==0?"Router":"Utilities"}</td>
|
<td data-label="Category">${plugin.Spec.type==0?"Router":"Utilities"}</td>
|
||||||
<td data-label="Action">
|
<td data-label="Action">
|
||||||
<div class="ui toggle checkbox">
|
<div class="ui small basic buttons">
|
||||||
<input type="checkbox" name="enable" ${plugin.Enabled ? 'checked' : ''}>
|
<button onclick="stopPlugin('${plugin.Spec.id}', this);" class="ui button pluginEnableButton" pluginid="${plugin.Spec.id}" ${plugin.Enabled ? '' : 'style="display:none;"'}>
|
||||||
|
<i class="red stop circle icon"></i> Stop
|
||||||
|
</button>
|
||||||
|
<button onclick="startPlugin('${plugin.Spec.id}', this);" class="ui button pluginDisableButton" pluginid="${plugin.Spec.id}" ${plugin.Enabled ? 'style="display:none;"' : ''}>
|
||||||
|
<i class="green play circle icon"></i> Start
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
tbody.append(row);
|
$("#pluginTable").append(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (data.length == 0){
|
||||||
|
$("#pluginTable").append(`
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" style="text-align: center;"><i class="ui green circle check icon"></i> No plugins installed</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(data);
|
console.log(data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initiatePluginList();
|
initiatePluginList();
|
||||||
|
|
||||||
|
function startPlugin(pluginId, btn=undefined){
|
||||||
|
if (btn) {
|
||||||
|
$(btn).html('<i class="spinner loading icon"></i> Starting');
|
||||||
|
$(btn).addClass('disabled');
|
||||||
|
}
|
||||||
|
$.cjax({
|
||||||
|
url: '/api/plugins/enable',
|
||||||
|
type: 'POST',
|
||||||
|
data: {plugin_id: pluginId},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}else{
|
||||||
|
msgbox("Plugin started", true);
|
||||||
|
}
|
||||||
|
initiatePluginList();
|
||||||
|
initPluginSideMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPlugin(pluginId, btn=undefined){
|
||||||
|
if (btn) {
|
||||||
|
$(btn).html('<i class="spinner loading icon"></i> Stopping');
|
||||||
|
$(btn).addClass('disabled');
|
||||||
|
}
|
||||||
|
$.cjax({
|
||||||
|
url: '/api/plugins/disable',
|
||||||
|
type: 'POST',
|
||||||
|
data: {plugin_id: pluginId},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}else{
|
||||||
|
msgbox("Plugin stopped", true);
|
||||||
|
}
|
||||||
|
initiatePluginList();
|
||||||
|
initPluginSideMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@
|
|||||||
<div class="ui divider menudivider">Plugins</div>
|
<div class="ui divider menudivider">Plugins</div>
|
||||||
<cx id="pluginMenu"></container>
|
<cx id="pluginMenu"></container>
|
||||||
<a class="item" style="pointer-events: none; user-select: none; opacity: 0.6;">
|
<a class="item" style="pointer-events: none; user-select: none; opacity: 0.6;">
|
||||||
<i class="green circle check icon"></i> No Installed Plugins
|
<i class="green circle check icon"></i> No Plugins Installed
|
||||||
</a>
|
</a>
|
||||||
</cx>
|
</cx>
|
||||||
<!-- Add more components here -->
|
<!-- Add more components here -->
|
||||||
|
Loading…
x
Reference in New Issue
Block a user