Browse Source

feat(modules): retrieve nginx modules status

Jacky 1 ngày trước cách đây
mục cha
commit
adf6f80061

+ 17 - 0
api/nginx/modules.go

@@ -0,0 +1,17 @@
+package nginx
+
+import (
+	"net/http"
+
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/gin-gonic/gin"
+)
+
+func GetModules(c *gin.Context) {
+	modules := nginx.GetModules()
+	modulesList := make([]nginx.Module, 0, modules.Len())
+	for _, module := range modules.AllFromFront() {
+		modulesList = append(modulesList, module)
+	}
+	c.JSON(http.StatusOK, modulesList)
+}

+ 2 - 0
api/nginx/router.go

@@ -27,4 +27,6 @@ func InitRouter(r *gin.RouterGroup) {
 	// Performance optimization endpoints
 	r.GET("nginx/performance", GetPerformanceSettings)
 	r.POST("nginx/performance", UpdatePerformanceSettings)
+
+	r.GET("nginx/modules", GetModules)
 }

+ 11 - 0
app/src/api/ngx.ts

@@ -104,6 +104,13 @@ export interface NginxPerfOpt {
   proxy_cache: ProxyCacheConfig
 }
 
+export interface NgxModule {
+  name: string
+  params?: string
+  dynamic: boolean
+  loaded: boolean
+}
+
 const ngx = {
   build_config(ngxConfig: NgxConfig) {
     return http.post('/ngx/build_config', ngxConfig)
@@ -152,6 +159,10 @@ const ngx = {
   update_performance(params: NginxPerfOpt): Promise<NginxConfigInfo> {
     return http.post('/nginx/performance', params)
   },
+
+  get_modules(): Promise<NgxModule[]> {
+    return http.get('/nginx/modules')
+  },
 }
 
 export default ngx

+ 28 - 3
app/src/layouts/SideBar.vue

@@ -1,10 +1,12 @@
 <script setup lang="ts">
+import type { NgxModule } from '@/api/ngx'
 import type { IconComponentProps } from '@ant-design/icons-vue/es/components/Icon'
 import type { AntdIconType } from '@ant-design/icons-vue/lib/components/AntdIcon'
 import type { Key } from 'ant-design-vue/es/_util/type'
-import type { ComputedRef, Ref } from 'vue'
-import EnvIndicator from '@/components/EnvIndicator/EnvIndicator.vue'
-import Logo from '@/components/Logo/Logo.vue'
+import ngx from '@/api/ngx'
+import EnvIndicator from '@/components/EnvIndicator'
+import Logo from '@/components/Logo'
+import { useGlobalStore } from '@/pinia/moudule/global'
 import { routes } from '@/routes'
 
 const route = useRoute()
@@ -47,6 +49,19 @@ interface Sidebar {
   children: Sidebar[]
 }
 
+const globalStore = useGlobalStore()
+const { modules, modulesMap } = storeToRefs(globalStore)
+
+onMounted(() => {
+  ngx.get_modules().then(r => {
+    modules.value = r
+    modulesMap.value = r.reduce((acc, m) => {
+      acc[m.name] = m
+      return acc
+    }, {} as Record<string, NgxModule>)
+  })
+})
+
 const visible: ComputedRef<Sidebar[]> = computed(() => {
   const res: Sidebar[] = [];
 
@@ -56,6 +71,11 @@ const visible: ComputedRef<Sidebar[]> = computed(() => {
       return
     }
 
+    if (s.meta && s.meta.modules && s.meta.modules?.length > 0
+      && !s.meta.modules.every(m => modulesMap.value[m]?.loaded)) {
+      return
+    }
+
     const t: Sidebar = {
       path: s.path,
       name: s.name as string,
@@ -69,6 +89,11 @@ const visible: ComputedRef<Sidebar[]> = computed(() => {
         return
       }
 
+      if (c.meta && c.meta.modules && c.meta.modules?.length > 0
+        && !c.meta.modules.every(m => modulesMap.value[m]?.loaded)) {
+        return
+      }
+
       t.children.push((c as unknown as Sidebar))
     })
     res.push(t)

+ 7 - 0
app/src/pinia/moudule/global.ts

@@ -1,3 +1,4 @@
+import type { NgxModule } from '@/api/ngx'
 import type { NginxStatus } from '@/constants'
 import { defineStore } from 'pinia'
 
@@ -15,8 +16,14 @@ export const useGlobalStore = defineStore('global', () => {
     index_scanning: false,
     auto_cert_processing: false,
   })
+
+  const modules = ref<NgxModule[]>([])
+  const modulesMap = ref<Record<string, NgxModule>>({})
+
   return {
     nginxStatus,
     processingStatus,
+    modules,
+    modulesMap,
   }
 })

+ 2 - 0
app/src/routes/modules/streams.ts

@@ -9,6 +9,7 @@ export const streamsRoutes: RouteRecordRaw[] = [
     meta: {
       name: () => $gettext('Manage Streams'),
       icon: ShareAltOutlined,
+      modules: ['stream'],
     },
   },
   {
@@ -19,6 +20,7 @@ export const streamsRoutes: RouteRecordRaw[] = [
       name: () => $gettext('Edit Stream'),
       hiddenInSidebar: true,
       lastRouteName: 'Manage Streams',
+      modules: ['stream'],
     },
   },
 ]

+ 1 - 0
app/src/routes/type.d.ts

@@ -17,5 +17,6 @@ declare module 'vue-router' {
     status_code?: number
     error?: () => string
     lastRouteName?: string
+    modules?: string[]
   }
 }

+ 2 - 0
internal/kernel/boot.go

@@ -19,6 +19,7 @@ import (
 	"github.com/0xJacky/Nginx-UI/internal/helper"
 	"github.com/0xJacky/Nginx-UI/internal/mcp"
 	"github.com/0xJacky/Nginx-UI/internal/passkey"
+	"github.com/0xJacky/Nginx-UI/internal/self_check"
 	"github.com/0xJacky/Nginx-UI/internal/validation"
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/query"
@@ -43,6 +44,7 @@ func Boot(ctx context.Context) {
 		InitNodeSecret,
 		InitCryptoSecret,
 		validation.Init,
+		self_check.Init,
 		func() {
 			InitDatabase(ctx)
 			cache.Init(ctx)

+ 11 - 0
internal/nginx/config_args.go

@@ -49,6 +49,17 @@ func getNginxV() string {
 	return string(out)
 }
 
+// getNginxT executes nginx -T and returns the output
+func getNginxT() string {
+	exePath := getNginxExePath()
+	out, err := execCommand(exePath, "-T")
+	if err != nil {
+		logger.Error(err)
+		return ""
+	}
+	return out
+}
+
 // Resolves relative paths by joining them with the nginx executable directory on Windows
 func resolvePath(path string) string {
 	if path == "" {

+ 199 - 26
internal/nginx/modules.go

@@ -1,44 +1,217 @@
 package nginx
 
 import (
+	"os"
 	"regexp"
 	"strings"
+	"sync"
+	"time"
+
+	"github.com/elliotchance/orderedmap/v3"
 )
 
 const (
-	ModuleStream = "stream_module"
+	ModuleStream = "stream"
 )
 
-func GetModules() (modules []string) {
-	out := getNginxV()
-	
-	// Regular expression to find modules in nginx -V output
-	r := regexp.MustCompile(`--with-([a-zA-Z0-9_-]+)(_module)?`)
-	
-	// Find all matches
-	matches := r.FindAllStringSubmatch(out, -1)
-	
-	// Extract module names from matches
+type Module struct {
+	Name    string `json:"name"`
+	Params  string `json:"params,omitempty"`
+	Dynamic bool   `json:"dynamic"`
+	Loaded  bool   `json:"loaded"`
+}
+
+// modulesCache stores the cached modules list and related metadata
+var (
+	modulesCache     = orderedmap.NewOrderedMap[string, Module]()
+	modulesCacheLock sync.RWMutex
+	lastPIDPath      string
+	lastPIDModTime   time.Time
+	lastPIDSize      int64
+)
+
+// clearModulesCache clears the modules cache
+func clearModulesCache() {
+	modulesCacheLock.Lock()
+	defer modulesCacheLock.Unlock()
+
+	modulesCache = orderedmap.NewOrderedMap[string, Module]()
+	lastPIDPath = ""
+	lastPIDModTime = time.Time{}
+	lastPIDSize = 0
+}
+
+// isPIDFileChanged checks if the PID file has changed since the last check
+func isPIDFileChanged() bool {
+	pidPath := GetPIDPath()
+
+	// If PID path has changed, consider it changed
+	if pidPath != lastPIDPath {
+		return true
+	}
+
+	// If Nginx is not running, consider PID changed
+	if !IsNginxRunning() {
+		return true
+	}
+
+	// Check if PID file has changed (modification time or size)
+	fileInfo, err := os.Stat(pidPath)
+	if err != nil {
+		return true
+	}
+
+	modTime := fileInfo.ModTime()
+	size := fileInfo.Size()
+
+	return modTime != lastPIDModTime || size != lastPIDSize
+}
+
+// updatePIDFileInfo updates the stored PID file information
+func updatePIDFileInfo() {
+	pidPath := GetPIDPath()
+
+	if fileInfo, err := os.Stat(pidPath); err == nil {
+		modulesCacheLock.Lock()
+		defer modulesCacheLock.Unlock()
+
+		lastPIDPath = pidPath
+		lastPIDModTime = fileInfo.ModTime()
+		lastPIDSize = fileInfo.Size()
+	}
+}
+
+// updateDynamicModulesStatus checks which dynamic modules are actually loaded in the running Nginx
+func updateDynamicModulesStatus() {
+	modulesCacheLock.Lock()
+	defer modulesCacheLock.Unlock()
+
+	// If cache is empty, there's nothing to update
+	if modulesCache.Len() == 0 {
+		return
+	}
+
+	// Get nginx -T output to check for loaded modules
+	out := getNginxT()
+	if out == "" {
+		return
+	}
+
+	// Regular expression to find loaded dynamic modules in nginx -T output
+	// Look for lines like "load_module modules/ngx_http_image_filter_module.so;"
+	loadModuleRe := regexp.MustCompile(`load_module\s+(?:modules/|/.*/)([a-zA-Z0-9_-]+)\.so;`)
+	matches := loadModuleRe.FindAllStringSubmatch(out, -1)
+
+	// Create a map of loaded dynamic modules
+	loadedDynamicModules := make(map[string]bool)
 	for _, match := range matches {
-		module := match[1]
-		// If the module doesn't end with "_module", add it
-		if !strings.HasSuffix(module, "_module") {
-			module = module + "_module"
+		if len(match) > 1 {
+			// Extract the module name without path and suffix
+			moduleName := match[1]
+			// Some normalization to match format in GetModules
+			moduleName = strings.TrimPrefix(moduleName, "ngx_")
+			moduleName = strings.TrimSuffix(moduleName, "_module")
+			loadedDynamicModules[moduleName] = true
 		}
-		modules = append(modules, module)
 	}
 
-	return modules
+	// Update the status for each module in the cache
+	for key := range modulesCache.Keys() {
+		// If the module is already marked as dynamic, check if it's actually loaded
+		if loadedDynamicModules[key] {
+			modulesCache.Set(key, Module{
+				Name:    key,
+				Dynamic: true,
+				Loaded:  true,
+			})
+		}
+	}
 }
 
-func IsModuleLoaded(module string) bool {
-	modules := GetModules()
-	
-	for _, m := range modules {
-		if m == module {
-			return true
+func GetModules() *orderedmap.OrderedMap[string, Module] {
+	modulesCacheLock.RLock()
+	cachedModules := modulesCache
+	modulesCacheLock.RUnlock()
+
+	// If we have cached modules and PID file hasn't changed, return cached modules
+	if cachedModules.Len() > 0 && !isPIDFileChanged() {
+		return cachedModules
+	}
+
+	// If PID has changed or we don't have cached modules, get fresh modules
+	out := getNginxV()
+
+	// Regular expression to find built-in modules in nginx -V output
+	builtinRe := regexp.MustCompile(`--with-([a-zA-Z0-9_-]+)(_module)?`)
+	builtinMatches := builtinRe.FindAllStringSubmatch(out, -1)
+
+	// Extract built-in module names from matches and put in map for quick lookup
+	moduleMap := make(map[string]bool)
+	for _, match := range builtinMatches {
+		if len(match) > 1 {
+			module := match[1]
+			moduleMap[module] = true
 		}
 	}
-	
-	return false
-}
+
+	// Regular expression to find dynamic modules in nginx -V output
+	dynamicRe := regexp.MustCompile(`--with-([a-zA-Z0-9_-]+)(_module)?=dynamic`)
+	dynamicMatches := dynamicRe.FindAllStringSubmatch(out, -1)
+
+	// Extract dynamic module names from matches
+	for _, match := range dynamicMatches {
+		if len(match) > 1 {
+			module := match[1]
+			// Only add if not already in list (to avoid duplicates)
+			if !moduleMap[module] {
+				moduleMap[module] = true
+			}
+		}
+	}
+
+	// Update cache
+	modulesCacheLock.Lock()
+	modulesCache = orderedmap.NewOrderedMap[string, Module]()
+	for module := range moduleMap {
+		// Mark modules as built-in (loaded) or dynamic (potentially not loaded)
+		if strings.Contains(out, "--with-"+module+"=dynamic") {
+			modulesCache.Set(module, Module{
+				Name:    module,
+				Dynamic: true,
+				Loaded:  true,
+			})
+		} else {
+			modulesCache.Set(module, Module{
+				Name:    module,
+				Dynamic: true,
+			})
+		}
+	}
+	modulesCacheLock.Unlock()
+
+	// Update dynamic modules status by checking if they're actually loaded
+	updateDynamicModulesStatus()
+
+	// Update PID file info
+	updatePIDFileInfo()
+
+	return modulesCache
+}
+
+// IsModuleLoaded checks if a module is loaded in Nginx
+func IsModuleLoaded(module string) bool {
+	// Ensure modules are in the cache
+	if modulesCache.Len() == 0 {
+		GetModules()
+	}
+
+	modulesCacheLock.RLock()
+	defer modulesCacheLock.RUnlock()
+
+	status, exists := modulesCache.Get(module)
+	if !exists {
+		return false
+	}
+
+	return status.Loaded
+}

+ 7 - 0
internal/nginx/nginx.go

@@ -29,6 +29,10 @@ func TestConfig() (stdOut string, stdErr error) {
 func Reload() (stdOut string, stdErr error) {
 	mutex.Lock()
 	defer mutex.Unlock()
+
+	// Clear the modules cache when reloading Nginx
+	clearModulesCache()
+
 	if settings.NginxSettings.ReloadCmd != "" {
 		return execShell(settings.NginxSettings.ReloadCmd)
 	}
@@ -40,6 +44,9 @@ func Restart() {
 	mutex.Lock()
 	defer mutex.Unlock()
 
+	// Clear the modules cache when restarting Nginx
+	clearModulesCache()
+
 	// fix(docker): nginx restart always output network error
 	time.Sleep(500 * time.Millisecond)
 

+ 1 - 1
internal/self_check/tasks.go

@@ -104,7 +104,7 @@ var selfCheckTasks = []*Task{
 
 var selfCheckTaskMap = orderedmap.NewOrderedMap[string, *Task]()
 
-func init() {
+func Init() {
 	if nginx.IsModuleLoaded(nginx.ModuleStream) {
 		selfCheckTasks = append(selfCheckTasks, &Task{
 			Key:  "Directory-Streams",