feat(modules): retrieve nginx modules status

This commit is contained in:
Jacky 2025-05-09 10:24:45 +00:00
parent 5b0cbf98e1
commit adf6f80061
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
12 changed files with 292 additions and 34 deletions

17
api/nginx/modules.go Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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'],
},
},
]

View file

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

View file

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

View file

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

View file

@ -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
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"
}
modules = append(modules, module)
}
return modules
type Module struct {
Name string `json:"name"`
Params string `json:"params,omitempty"`
Dynamic bool `json:"dynamic"`
Loaded bool `json:"loaded"`
}
func IsModuleLoaded(module string) bool {
modules := GetModules()
for _, m := range modules {
if m == module {
return true
// 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 {
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
}
}
return false
}
// 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 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
}
}
// 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
}

View file

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

View file

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