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

@ -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)
}
} }

View File

@ -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)
}()
})
}

View File

@ -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)
}()
}

View File

@ -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())

View 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

View File

@ -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)
}()
})
}

View File

@ -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)
}()
}

View File

@ -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"

View File

@ -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()

View File

@ -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
} }

View File

@ -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)

View File

@ -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)
}()
})
}

View File

@ -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)
}()
}

View File

@ -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);
} }

View File

@ -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>

View File

@ -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 -->