mirror of
https://github.com/tnb-labs/panel.git
synced 2026-09-01 14:55:12 +08:00
feat: PHP 添加 FPM 进程/OPcache/Composer 管理
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,12 @@ type EnvironmentPHPUpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// EnvironmentPHPComposerMirror Composer 镜像源设置
|
||||
type EnvironmentPHPComposerMirror struct {
|
||||
Version uint `json:"version"`
|
||||
Mirror string `form:"mirror" json:"mirror"`
|
||||
}
|
||||
|
||||
// EnvironmentPHPConfigTune PHP 配置调整
|
||||
type EnvironmentPHPConfigTune struct {
|
||||
Version uint `json:"version"`
|
||||
|
||||
@@ -85,6 +85,18 @@ func EnvironmentRoutes(environmentDotnetService *service.EnvironmentDotnetServic
|
||||
Summary: "更新 PHP 配置调优", Tags: tags, Request: request.EnvironmentPHPConfigTune{}},
|
||||
{Method: http.MethodPost, Path: "/api/environment/php/{version}/clean_session", Handler: environmentPHP.CleanSession,
|
||||
Summary: "清理 PHP Session", Tags: tags, Request: request.EnvironmentPHPVersion{}},
|
||||
{Method: http.MethodGet, Path: "/api/environment/php/{version}/processes", Handler: environmentPHP.Processes,
|
||||
Summary: "获取 PHP-FPM 进程列表", Tags: tags, Request: request.EnvironmentPHPVersion{}},
|
||||
{Method: http.MethodGet, Path: "/api/environment/php/{version}/opcache", Handler: environmentPHP.Opcache,
|
||||
Summary: "获取 OPcache 状态", Tags: tags, Request: request.EnvironmentPHPVersion{}},
|
||||
{Method: http.MethodPost, Path: "/api/environment/php/{version}/opcache/reset", Handler: environmentPHP.ResetOpcache,
|
||||
Summary: "重置 OPcache", Tags: tags, Request: request.EnvironmentPHPVersion{}},
|
||||
{Method: http.MethodGet, Path: "/api/environment/php/{version}/composer", Handler: environmentPHP.Composer,
|
||||
Summary: "获取 Composer 状态", Tags: tags, Request: request.EnvironmentPHPVersion{}},
|
||||
{Method: http.MethodPost, Path: "/api/environment/php/{version}/composer/install", Handler: environmentPHP.InstallComposer,
|
||||
Summary: "安装 Composer", Tags: tags, Request: request.EnvironmentPHPVersion{}},
|
||||
{Method: http.MethodPost, Path: "/api/environment/php/{version}/composer/mirror", Handler: environmentPHP.SetComposerMirror,
|
||||
Summary: "设置 Composer 镜像源", Tags: tags, Request: request.EnvironmentPHPComposerMirror{}},
|
||||
|
||||
// Python
|
||||
{Method: http.MethodPost, Path: "/api/environment/python/{slug}/set_cli", Handler: environmentPython.SetCli,
|
||||
|
||||
+411
-152
@@ -1,7 +1,10 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
@@ -19,8 +22,10 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/fastcgi"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/tools"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
@@ -355,6 +360,364 @@ func (s *EnvironmentPHPService) UninstallModule(w http.ResponseWriter, r *http.R
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 PHP 配置调整参数
|
||||
func (s *EnvironmentPHPService) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentPHPVersion](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !s.environmentRepo.IsInstalled("php", strconv.FormatUint(uint64(req.Version), 10)) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
|
||||
return
|
||||
}
|
||||
|
||||
iniPath := fmt.Sprintf("%s/server/php/%d/etc/php.ini", app.Root, req.Version)
|
||||
fpmPath := fmt.Sprintf("%s/server/php/%d/etc/php-fpm.conf", app.Root, req.Version)
|
||||
|
||||
ini, err := io.Read(iniPath)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
fpm, err := io.Read(fpmPath)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := request.EnvironmentPHPConfigTune{
|
||||
// php.ini 常规设置
|
||||
ShortOpenTag: confval.PHPINI.Get(ini, "short_open_tag"),
|
||||
DateTimezone: confval.PHPINI.Get(ini, "date.timezone"),
|
||||
DisplayErrors: confval.PHPINI.Get(ini, "display_errors"),
|
||||
ErrorReporting: confval.PHPINI.Get(ini, "error_reporting"),
|
||||
// php.ini 禁用函数
|
||||
DisableFunctions: confval.PHPINI.Get(ini, "disable_functions"),
|
||||
// php.ini 上传限制
|
||||
UploadMaxFilesize: confval.PHPINI.Get(ini, "upload_max_filesize"),
|
||||
PostMaxSize: confval.PHPINI.Get(ini, "post_max_size"),
|
||||
MaxFileUploads: confval.PHPINI.Get(ini, "max_file_uploads"),
|
||||
MemoryLimit: confval.PHPINI.Get(ini, "memory_limit"),
|
||||
// php.ini 超时限制
|
||||
MaxExecutionTime: confval.PHPINI.Get(ini, "max_execution_time"),
|
||||
MaxInputTime: confval.PHPINI.Get(ini, "max_input_time"),
|
||||
MaxInputVars: confval.PHPINI.Get(ini, "max_input_vars"),
|
||||
// Session 相关
|
||||
SessionSaveHandler: confval.PHPINI.Get(ini, "session.save_handler"),
|
||||
SessionSavePath: confval.PHPINI.Get(ini, "session.save_path"),
|
||||
SessionGcMaxlifetime: confval.PHPINI.Get(ini, "session.gc_maxlifetime"),
|
||||
SessionCookieLifetime: confval.PHPINI.Get(ini, "session.cookie_lifetime"),
|
||||
// php-fpm.conf 配置
|
||||
Pm: confval.PHPINI.Get(fpm, "pm"),
|
||||
PmMaxChildren: confval.PHPINI.Get(fpm, "pm.max_children"),
|
||||
PmStartServers: confval.PHPINI.Get(fpm, "pm.start_servers"),
|
||||
PmMinSpareServers: confval.PHPINI.Get(fpm, "pm.min_spare_servers"),
|
||||
PmMaxSpareServers: confval.PHPINI.Get(fpm, "pm.max_spare_servers"),
|
||||
}
|
||||
|
||||
Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 PHP 配置调整参数
|
||||
func (s *EnvironmentPHPService) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentPHPConfigTune](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !s.environmentRepo.IsInstalled("php", strconv.FormatUint(uint64(req.Version), 10)) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
|
||||
return
|
||||
}
|
||||
|
||||
iniPath := fmt.Sprintf("%s/server/php/%d/etc/php.ini", app.Root, req.Version)
|
||||
fpmPath := fmt.Sprintf("%s/server/php/%d/etc/php-fpm.conf", app.Root, req.Version)
|
||||
|
||||
ini, err := io.Read(iniPath)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
fpm, err := io.Read(fpmPath)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新 php.ini 配置
|
||||
ini = confval.PHPINI.Set(ini, "short_open_tag", req.ShortOpenTag)
|
||||
ini = confval.PHPINI.Set(ini, "date.timezone", req.DateTimezone)
|
||||
ini = confval.PHPINI.Set(ini, "display_errors", req.DisplayErrors)
|
||||
ini = confval.PHPINI.Set(ini, "error_reporting", req.ErrorReporting)
|
||||
ini = confval.PHPINI.Set(ini, "disable_functions", req.DisableFunctions)
|
||||
ini = confval.PHPINI.Set(ini, "upload_max_filesize", req.UploadMaxFilesize)
|
||||
ini = confval.PHPINI.Set(ini, "post_max_size", req.PostMaxSize)
|
||||
ini = confval.PHPINI.Set(ini, "max_execution_time", req.MaxExecutionTime)
|
||||
ini = confval.PHPINI.Set(ini, "max_input_time", req.MaxInputTime)
|
||||
ini = confval.PHPINI.Set(ini, "memory_limit", req.MemoryLimit)
|
||||
ini = confval.PHPINI.Set(ini, "max_input_vars", req.MaxInputVars)
|
||||
ini = confval.PHPINI.Set(ini, "max_file_uploads", req.MaxFileUploads)
|
||||
ini = confval.PHPINI.Set(ini, "session.save_handler", req.SessionSaveHandler)
|
||||
ini = confval.PHPINI.Set(ini, "session.save_path", req.SessionSavePath)
|
||||
ini = confval.PHPINI.Set(ini, "session.gc_maxlifetime", req.SessionGcMaxlifetime)
|
||||
ini = confval.PHPINI.Set(ini, "session.cookie_lifetime", req.SessionCookieLifetime)
|
||||
|
||||
// 更新 php-fpm.conf 配置
|
||||
fpm = confval.PHPINI.Set(fpm, "pm", req.Pm)
|
||||
fpm = confval.PHPINI.Set(fpm, "pm.max_children", req.PmMaxChildren)
|
||||
fpm = confval.PHPINI.Set(fpm, "pm.start_servers", req.PmStartServers)
|
||||
fpm = confval.PHPINI.Set(fpm, "pm.min_spare_servers", req.PmMinSpareServers)
|
||||
fpm = confval.PHPINI.Set(fpm, "pm.max_spare_servers", req.PmMaxSpareServers)
|
||||
|
||||
if err = io.Write(iniPath, ini, 0644); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.Write(fpmPath, fpm, 0644); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// CleanSession 清理 PHP Session 文件
|
||||
func (s *EnvironmentPHPService) CleanSession(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentPHPVersion](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !s.environmentRepo.IsInstalled("php", strconv.FormatUint(uint64(req.Version), 10)) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
|
||||
return
|
||||
}
|
||||
|
||||
iniPath := fmt.Sprintf("%s/server/php/%d/etc/php.ini", app.Root, req.Version)
|
||||
ini, err := io.Read(iniPath)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
handler := confval.PHPINI.Get(ini, "session.save_handler")
|
||||
if handler != "files" {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("Session save handler is not files, cannot clean"))
|
||||
return
|
||||
}
|
||||
|
||||
savePath := confval.PHPINI.Get(ini, "session.save_path")
|
||||
if savePath == "" {
|
||||
savePath = "/tmp"
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("find '%s' -name 'sess_*' -type f -delete", savePath); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// Processes 获取 PHP-FPM 工作进程列表
|
||||
func (s *EnvironmentPHPService) Processes(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentPHPVersion](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !s.environmentRepo.IsInstalled("php", strconv.FormatUint(uint64(req.Version), 10)) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
|
||||
return
|
||||
}
|
||||
|
||||
var raw struct {
|
||||
Processes []struct {
|
||||
PID int64 `json:"pid"`
|
||||
State string `json:"state"`
|
||||
StartSince int64 `json:"start since"`
|
||||
Requests int64 `json:"requests"`
|
||||
RequestDuration int64 `json:"request duration"`
|
||||
Method string `json:"request method"`
|
||||
URI string `json:"request uri"`
|
||||
Script string `json:"script"`
|
||||
LastCPU float64 `json:"last request cpu"`
|
||||
LastMemory int64 `json:"last request memory"`
|
||||
} `json:"processes"`
|
||||
}
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
if _, err = client.R().SetResult(&raw).Get(fmt.Sprintf("http://127.0.0.1/phpfpm_status/%d?json&full", req.Version)); err != nil {
|
||||
Success(w, []types.EnvironmentPHPProcess{})
|
||||
return
|
||||
}
|
||||
|
||||
processes := make([]types.EnvironmentPHPProcess, 0, len(raw.Processes))
|
||||
for _, item := range raw.Processes {
|
||||
processes = append(processes, types.EnvironmentPHPProcess{
|
||||
PID: item.PID,
|
||||
State: item.State,
|
||||
StartSince: item.StartSince,
|
||||
Requests: item.Requests,
|
||||
RequestDuration: item.RequestDuration,
|
||||
Method: item.Method,
|
||||
URI: item.URI,
|
||||
Script: item.Script,
|
||||
LastRequestCPU: item.LastCPU,
|
||||
LastRequestMem: item.LastMemory,
|
||||
})
|
||||
}
|
||||
|
||||
Success(w, processes)
|
||||
}
|
||||
|
||||
// Opcache 获取 OPcache 状态
|
||||
func (s *EnvironmentPHPService) Opcache(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentPHPVersion](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !s.environmentRepo.IsInstalled("php", strconv.FormatUint(uint64(req.Version), 10)) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
|
||||
return
|
||||
}
|
||||
|
||||
body, err := s.opcacheProbe(r.Context(), req.Version, "")
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to get OPcache status: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// OPcache 未启用时探针返回 false,无法解析为对象
|
||||
var raw map[string]any
|
||||
if err = json.Unmarshal(body, &raw); err != nil || raw == nil {
|
||||
Success(w, types.EnvironmentPHPOpcache{Enabled: false})
|
||||
return
|
||||
}
|
||||
|
||||
memory := cast.ToStringMap(raw["memory_usage"])
|
||||
stats := cast.ToStringMap(raw["opcache_statistics"])
|
||||
jit := cast.ToStringMap(raw["jit"])
|
||||
|
||||
Success(w, types.EnvironmentPHPOpcache{
|
||||
Enabled: cast.ToBool(raw["opcache_enabled"]),
|
||||
MemoryUsed: tools.FormatBytes(cast.ToFloat64(memory["used_memory"])),
|
||||
MemoryFree: tools.FormatBytes(cast.ToFloat64(memory["free_memory"])),
|
||||
MemoryWasted: tools.FormatBytes(cast.ToFloat64(memory["wasted_memory"])),
|
||||
WastedPercent: math.Round(cast.ToFloat64(memory["current_wasted_percentage"])*100) / 100,
|
||||
HitRate: math.Round(cast.ToFloat64(stats["opcache_hit_rate"])*100) / 100,
|
||||
Hits: cast.ToInt64(stats["hits"]),
|
||||
Misses: cast.ToInt64(stats["misses"]),
|
||||
CachedScripts: cast.ToInt64(stats["num_cached_scripts"]),
|
||||
CachedKeys: cast.ToInt64(stats["num_cached_keys"]),
|
||||
MaxCachedKeys: cast.ToInt64(stats["max_cached_keys"]),
|
||||
OomRestarts: cast.ToInt64(stats["oom_restarts"]),
|
||||
JitEnabled: cast.ToBool(jit["enabled"]) && cast.ToBool(jit["on"]),
|
||||
JitBufferSize: tools.FormatBytes(cast.ToFloat64(jit["buffer_size"])),
|
||||
JitBufferFree: tools.FormatBytes(cast.ToFloat64(jit["buffer_free"])),
|
||||
})
|
||||
}
|
||||
|
||||
// ResetOpcache 重置 OPcache
|
||||
func (s *EnvironmentPHPService) ResetOpcache(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentPHPVersion](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !s.environmentRepo.IsInstalled("php", strconv.FormatUint(uint64(req.Version), 10)) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
|
||||
return
|
||||
}
|
||||
|
||||
body, err := s.opcacheProbe(r.Context(), req.Version, "action=reset")
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to reset OPcache: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
var raw map[string]any
|
||||
if err = json.Unmarshal(body, &raw); err != nil || !cast.ToBool(raw["reset"]) {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to reset OPcache, it may not be enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// Composer 获取 Composer 状态
|
||||
func (s *EnvironmentPHPService) Composer(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentPHPVersion](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
composer := types.EnvironmentPHPComposer{Installed: io.Exists("/usr/local/bin/composer")}
|
||||
if composer.Installed {
|
||||
out, outErr := shell.ExecfWithEnv([]string{"COMPOSER_ALLOW_SUPERUSER=1"}, "%s/server/php/%d/bin/php /usr/local/bin/composer --version --no-ansi 2>/dev/null", app.Root, req.Version)
|
||||
// 输出形如 Composer version 2.8.4 2025-01-01 00:00:00
|
||||
if fields := strings.Fields(out); outErr == nil && len(fields) >= 3 {
|
||||
composer.Version = fields[2]
|
||||
}
|
||||
composer.Mirror = s.composerMirror()
|
||||
}
|
||||
|
||||
Success(w, composer)
|
||||
}
|
||||
|
||||
// InstallComposer 安装/更新 Composer(异步任务)
|
||||
func (s *EnvironmentPHPService) InstallComposer(w http.ResponseWriter, r *http.Request) {
|
||||
cmd := fmt.Sprintf(`curl -sSLm 10 --retry 3 'https://%s/php/composer.sh' | bash -s -- 'install'`, s.conf.App.DownloadEndpoint)
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = "php:composer"
|
||||
task.Name = s.t.Get("Install Composer")
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
if err := s.taskRepo.Push(task); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// SetComposerMirror 设置 Composer 全局镜像源
|
||||
func (s *EnvironmentPHPService) SetComposerMirror(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentPHPComposerMirror](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !io.Exists("/usr/local/bin/composer") {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("Composer is not installed"))
|
||||
return
|
||||
}
|
||||
|
||||
php := fmt.Sprintf("%s/server/php/%d/bin/php", app.Root, req.Version)
|
||||
env := []string{"COMPOSER_ALLOW_SUPERUSER=1"}
|
||||
if req.Mirror == "" {
|
||||
// 恢复官方源,未设置过镜像时报错可忽略
|
||||
_, _ = shell.ExecfWithEnv(env, "%s /usr/local/bin/composer config -g --unset repos.packagist", php)
|
||||
Success(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(req.Mirror, "https://") && !strings.HasPrefix(req.Mirror, "http://") {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid mirror url"))
|
||||
return
|
||||
}
|
||||
if _, err = shell.ExecfWithEnv(env, "%s /usr/local/bin/composer config -g repos.packagist composer '%s'", php, req.Mirror); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *EnvironmentPHPService) getModules(version uint) []types.EnvironmentPHPModule {
|
||||
modules := []types.EnvironmentPHPModule{
|
||||
{
|
||||
@@ -602,162 +965,58 @@ func (s *EnvironmentPHPService) checkModule(version uint, slug string) bool {
|
||||
})
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 PHP 配置调整参数
|
||||
func (s *EnvironmentPHPService) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentPHPVersion](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !s.environmentRepo.IsInstalled("php", strconv.FormatUint(uint64(req.Version), 10)) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
|
||||
return
|
||||
// opcacheProbe 通过 FastCGI 请求 PHP-FPM 执行 OPcache 探针脚本
|
||||
func (s *EnvironmentPHPService) opcacheProbe(ctx context.Context, version uint, query string) ([]byte, error) {
|
||||
probePath := "/tmp/acepanel_opcache_probe.php"
|
||||
probe := `<?php
|
||||
if (($_GET['action'] ?? '') === 'reset') {
|
||||
echo json_encode(['reset' => function_exists('opcache_reset') && opcache_reset()]);
|
||||
exit;
|
||||
}
|
||||
echo json_encode(function_exists('opcache_get_status') ? opcache_get_status(false) : false);
|
||||
`
|
||||
// FPM 以 www 用户执行,探针放在 /tmp 保证可读,每次覆盖写入保证内容正确
|
||||
if err := io.Write(probePath, probe, 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iniPath := fmt.Sprintf("%s/server/php/%d/etc/php.ini", app.Root, req.Version)
|
||||
fpmPath := fmt.Sprintf("%s/server/php/%d/etc/php-fpm.conf", app.Root, req.Version)
|
||||
|
||||
ini, err := io.Read(iniPath)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
fpm, err := io.Read(fpmPath)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := request.EnvironmentPHPConfigTune{
|
||||
// php.ini 常规设置
|
||||
ShortOpenTag: confval.PHPINI.Get(ini, "short_open_tag"),
|
||||
DateTimezone: confval.PHPINI.Get(ini, "date.timezone"),
|
||||
DisplayErrors: confval.PHPINI.Get(ini, "display_errors"),
|
||||
ErrorReporting: confval.PHPINI.Get(ini, "error_reporting"),
|
||||
// php.ini 禁用函数
|
||||
DisableFunctions: confval.PHPINI.Get(ini, "disable_functions"),
|
||||
// php.ini 上传限制
|
||||
UploadMaxFilesize: confval.PHPINI.Get(ini, "upload_max_filesize"),
|
||||
PostMaxSize: confval.PHPINI.Get(ini, "post_max_size"),
|
||||
MaxFileUploads: confval.PHPINI.Get(ini, "max_file_uploads"),
|
||||
MemoryLimit: confval.PHPINI.Get(ini, "memory_limit"),
|
||||
// php.ini 超时限制
|
||||
MaxExecutionTime: confval.PHPINI.Get(ini, "max_execution_time"),
|
||||
MaxInputTime: confval.PHPINI.Get(ini, "max_input_time"),
|
||||
MaxInputVars: confval.PHPINI.Get(ini, "max_input_vars"),
|
||||
// Session 相关
|
||||
SessionSaveHandler: confval.PHPINI.Get(ini, "session.save_handler"),
|
||||
SessionSavePath: confval.PHPINI.Get(ini, "session.save_path"),
|
||||
SessionGcMaxlifetime: confval.PHPINI.Get(ini, "session.gc_maxlifetime"),
|
||||
SessionCookieLifetime: confval.PHPINI.Get(ini, "session.cookie_lifetime"),
|
||||
// php-fpm.conf 配置
|
||||
Pm: confval.PHPINI.Get(fpm, "pm"),
|
||||
PmMaxChildren: confval.PHPINI.Get(fpm, "pm.max_children"),
|
||||
PmStartServers: confval.PHPINI.Get(fpm, "pm.start_servers"),
|
||||
PmMinSpareServers: confval.PHPINI.Get(fpm, "pm.min_spare_servers"),
|
||||
PmMaxSpareServers: confval.PHPINI.Get(fpm, "pm.max_spare_servers"),
|
||||
}
|
||||
|
||||
Success(w, tune)
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
return fastcgi.Request(timeoutCtx, "unix", fmt.Sprintf("/tmp/php-cgi-%d.sock", version), map[string]string{
|
||||
"SCRIPT_FILENAME": probePath,
|
||||
"SCRIPT_NAME": "/acepanel_opcache_probe.php",
|
||||
"REQUEST_METHOD": "GET",
|
||||
"QUERY_STRING": query,
|
||||
"SERVER_PROTOCOL": "HTTP/1.1",
|
||||
"GATEWAY_INTERFACE": "CGI/1.1",
|
||||
"REMOTE_ADDR": "127.0.0.1",
|
||||
"SERVER_ADDR": "127.0.0.1",
|
||||
"SERVER_PORT": "80",
|
||||
"SERVER_NAME": "localhost",
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 PHP 配置调整参数
|
||||
func (s *EnvironmentPHPService) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentPHPConfigTune](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !s.environmentRepo.IsInstalled("php", strconv.FormatUint(uint64(req.Version), 10)) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
|
||||
return
|
||||
// composerMirror 读取 Composer 全局镜像源配置,未设置时返回空
|
||||
func (s *EnvironmentPHPService) composerMirror() string {
|
||||
for _, path := range []string{"/root/.config/composer/config.json", "/root/.composer/config.json"} {
|
||||
content, err := io.Read(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var cfg struct {
|
||||
Repositories map[string]struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"repositories"`
|
||||
}
|
||||
if json.Unmarshal([]byte(content), &cfg) != nil {
|
||||
continue
|
||||
}
|
||||
for _, key := range []string{"packagist", "packagist.org"} {
|
||||
if repo, ok := cfg.Repositories[key]; ok && repo.URL != "" {
|
||||
return repo.URL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
iniPath := fmt.Sprintf("%s/server/php/%d/etc/php.ini", app.Root, req.Version)
|
||||
fpmPath := fmt.Sprintf("%s/server/php/%d/etc/php-fpm.conf", app.Root, req.Version)
|
||||
|
||||
ini, err := io.Read(iniPath)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
fpm, err := io.Read(fpmPath)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新 php.ini 配置
|
||||
ini = confval.PHPINI.Set(ini, "short_open_tag", req.ShortOpenTag)
|
||||
ini = confval.PHPINI.Set(ini, "date.timezone", req.DateTimezone)
|
||||
ini = confval.PHPINI.Set(ini, "display_errors", req.DisplayErrors)
|
||||
ini = confval.PHPINI.Set(ini, "error_reporting", req.ErrorReporting)
|
||||
ini = confval.PHPINI.Set(ini, "disable_functions", req.DisableFunctions)
|
||||
ini = confval.PHPINI.Set(ini, "upload_max_filesize", req.UploadMaxFilesize)
|
||||
ini = confval.PHPINI.Set(ini, "post_max_size", req.PostMaxSize)
|
||||
ini = confval.PHPINI.Set(ini, "max_execution_time", req.MaxExecutionTime)
|
||||
ini = confval.PHPINI.Set(ini, "max_input_time", req.MaxInputTime)
|
||||
ini = confval.PHPINI.Set(ini, "memory_limit", req.MemoryLimit)
|
||||
ini = confval.PHPINI.Set(ini, "max_input_vars", req.MaxInputVars)
|
||||
ini = confval.PHPINI.Set(ini, "max_file_uploads", req.MaxFileUploads)
|
||||
ini = confval.PHPINI.Set(ini, "session.save_handler", req.SessionSaveHandler)
|
||||
ini = confval.PHPINI.Set(ini, "session.save_path", req.SessionSavePath)
|
||||
ini = confval.PHPINI.Set(ini, "session.gc_maxlifetime", req.SessionGcMaxlifetime)
|
||||
ini = confval.PHPINI.Set(ini, "session.cookie_lifetime", req.SessionCookieLifetime)
|
||||
|
||||
// 更新 php-fpm.conf 配置
|
||||
fpm = confval.PHPINI.Set(fpm, "pm", req.Pm)
|
||||
fpm = confval.PHPINI.Set(fpm, "pm.max_children", req.PmMaxChildren)
|
||||
fpm = confval.PHPINI.Set(fpm, "pm.start_servers", req.PmStartServers)
|
||||
fpm = confval.PHPINI.Set(fpm, "pm.min_spare_servers", req.PmMinSpareServers)
|
||||
fpm = confval.PHPINI.Set(fpm, "pm.max_spare_servers", req.PmMaxSpareServers)
|
||||
|
||||
if err = io.Write(iniPath, ini, 0644); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.Write(fpmPath, fpm, 0644); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// CleanSession 清理 PHP Session 文件
|
||||
func (s *EnvironmentPHPService) CleanSession(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentPHPVersion](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !s.environmentRepo.IsInstalled("php", strconv.FormatUint(uint64(req.Version), 10)) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
|
||||
return
|
||||
}
|
||||
|
||||
iniPath := fmt.Sprintf("%s/server/php/%d/etc/php.ini", app.Root, req.Version)
|
||||
ini, err := io.Read(iniPath)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
handler := confval.PHPINI.Get(ini, "session.save_handler")
|
||||
if handler != "files" {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("Session save handler is not files, cannot clean"))
|
||||
return
|
||||
}
|
||||
|
||||
savePath := confval.PHPINI.Get(ini, "session.save_path")
|
||||
if savePath == "" {
|
||||
savePath = "/tmp"
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("find '%s' -name 'sess_*' -type f -delete", savePath); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
const (
|
||||
typeBeginRequest byte = 1
|
||||
typeEndRequest byte = 3
|
||||
typeParams byte = 4
|
||||
typeStdin byte = 5
|
||||
typeStdout byte = 6
|
||||
typeStderr byte = 7
|
||||
|
||||
roleResponder = 1
|
||||
)
|
||||
|
||||
// Request 向 FastCGI 服务发起一次 responder 请求,返回剥离 CGI 响应头后的 body
|
||||
func Request(ctx context.Context, network, address string, params map[string]string) ([]byte, error) {
|
||||
var dialer net.Dialer
|
||||
conn, err := dialer.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
_ = conn.SetDeadline(deadline)
|
||||
}
|
||||
|
||||
const requestID = 1
|
||||
// BEGIN_REQUEST:responder 角色,连接不复用
|
||||
if err = writeRecord(conn, typeBeginRequest, requestID, []byte{0, roleResponder, 0, 0, 0, 0, 0, 0}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
for name, value := range params {
|
||||
encodeNameValue(&buf, name, value)
|
||||
}
|
||||
if err = writeRecord(conn, typeParams, requestID, buf.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = writeRecord(conn, typeParams, requestID, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = writeRecord(conn, typeStdin, requestID, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
header := make([]byte, 8)
|
||||
for {
|
||||
if _, err = io.ReadFull(conn, header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contentLength := binary.BigEndian.Uint16(header[4:6])
|
||||
content := make([]byte, int(contentLength)+int(header[6]))
|
||||
if _, err = io.ReadFull(conn, content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch header[1] {
|
||||
case typeStdout:
|
||||
stdout.Write(content[:contentLength])
|
||||
case typeStderr:
|
||||
stderr.Write(content[:contentLength])
|
||||
case typeEndRequest:
|
||||
if stderr.Len() > 0 {
|
||||
return nil, fmt.Errorf("fastcgi stderr: %s", stderr.String())
|
||||
}
|
||||
// 剥离 CGI 响应头
|
||||
if _, body, found := bytes.Cut(stdout.Bytes(), []byte("\r\n\r\n")); found {
|
||||
return body, nil
|
||||
}
|
||||
return stdout.Bytes(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeRecord 写入一条 FastCGI 记录
|
||||
func writeRecord(w io.Writer, typ byte, requestID uint16, content []byte) error {
|
||||
header := [8]byte{1, typ}
|
||||
binary.BigEndian.PutUint16(header[2:4], requestID)
|
||||
binary.BigEndian.PutUint16(header[4:6], uint16(len(content))) //nolint:gosec // params 由面板构造,长度远小于 64KB
|
||||
if _, err := w.Write(header[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(content) > 0 {
|
||||
if _, err := w.Write(content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeNameValue 按 FastCGI name-value 格式编码
|
||||
func encodeNameValue(buf *bytes.Buffer, name, value string) {
|
||||
writeLength := func(n int) {
|
||||
if n < 128 {
|
||||
buf.WriteByte(byte(n))
|
||||
return
|
||||
}
|
||||
var b [4]byte
|
||||
binary.BigEndian.PutUint32(b[:], uint32(n)|1<<31) //nolint:gosec // 长度非负
|
||||
buf.Write(b[:])
|
||||
}
|
||||
writeLength(len(name))
|
||||
writeLength(len(value))
|
||||
buf.WriteString(name)
|
||||
buf.WriteString(value)
|
||||
}
|
||||
@@ -6,3 +6,43 @@ type EnvironmentPHPModule struct {
|
||||
Description string `json:"description"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
// EnvironmentPHPProcess PHP-FPM 工作进程信息
|
||||
type EnvironmentPHPProcess struct {
|
||||
PID int64 `json:"pid"`
|
||||
State string `json:"state"`
|
||||
StartSince int64 `json:"start_since"`
|
||||
Requests int64 `json:"requests"`
|
||||
RequestDuration int64 `json:"request_duration"` // 微秒
|
||||
Method string `json:"method"`
|
||||
URI string `json:"uri"`
|
||||
Script string `json:"script"`
|
||||
LastRequestCPU float64 `json:"last_request_cpu"`
|
||||
LastRequestMem int64 `json:"last_request_memory"`
|
||||
}
|
||||
|
||||
// EnvironmentPHPOpcache OPcache 状态
|
||||
type EnvironmentPHPOpcache struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
MemoryUsed string `json:"memory_used"`
|
||||
MemoryFree string `json:"memory_free"`
|
||||
MemoryWasted string `json:"memory_wasted"`
|
||||
WastedPercent float64 `json:"wasted_percent"`
|
||||
HitRate float64 `json:"hit_rate"`
|
||||
Hits int64 `json:"hits"`
|
||||
Misses int64 `json:"misses"`
|
||||
CachedScripts int64 `json:"cached_scripts"`
|
||||
CachedKeys int64 `json:"cached_keys"`
|
||||
MaxCachedKeys int64 `json:"max_cached_keys"`
|
||||
OomRestarts int64 `json:"oom_restarts"`
|
||||
JitEnabled bool `json:"jit_enabled"`
|
||||
JitBufferSize string `json:"jit_buffer_size"`
|
||||
JitBufferFree string `json:"jit_buffer_free"`
|
||||
}
|
||||
|
||||
// EnvironmentPHPComposer Composer 状态
|
||||
type EnvironmentPHPComposer struct {
|
||||
Installed bool `json:"installed"`
|
||||
Version string `json:"version"`
|
||||
Mirror string `json:"mirror"` // 空为官方源
|
||||
}
|
||||
|
||||
@@ -36,4 +36,17 @@ export default {
|
||||
http.Post(`/environment/php/${slug}/config_tune`, data),
|
||||
// 清理 Session 文件
|
||||
cleanSession: (slug: number): any => http.Post(`/environment/php/${slug}/clean_session`),
|
||||
// FPM 进程列表
|
||||
processes: (slug: number): any => http.Get(`/environment/php/${slug}/processes`),
|
||||
// OPcache 状态
|
||||
opcache: (slug: number): any => http.Get(`/environment/php/${slug}/opcache`),
|
||||
// 重置 OPcache
|
||||
resetOpcache: (slug: number): any => http.Post(`/environment/php/${slug}/opcache/reset`),
|
||||
// Composer 状态
|
||||
composer: (slug: number): any => http.Get(`/environment/php/${slug}/composer`),
|
||||
// 安装/更新 Composer
|
||||
installComposer: (slug: number): any => http.Post(`/environment/php/${slug}/composer/install`),
|
||||
// 设置 Composer 镜像源
|
||||
setComposerMirror: (slug: number, mirror: string): any =>
|
||||
http.Post(`/environment/php/${slug}/composer/mirror`, { mirror }),
|
||||
}
|
||||
|
||||
@@ -229,23 +229,25 @@ const handleResetTopSQL = async () => {
|
||||
<n-alert v-else-if="!topSQL.enabled && topSQL.pending_restart" type="warning">
|
||||
{{ $gettext('performance_schema is configured, restart the service to take effect.') }}
|
||||
</n-alert>
|
||||
<n-alert v-else-if="!topSQL.enabled" type="info">
|
||||
{{
|
||||
$gettext(
|
||||
'performance_schema is not enabled. After enabling, SQL performance statistics will be collected, which increases memory usage and requires a restart.',
|
||||
)
|
||||
}}
|
||||
<n-button
|
||||
class="ml-16"
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="enableTopSQLLoading"
|
||||
:disabled="enableTopSQLLoading"
|
||||
@click="handleEnableTopSQL"
|
||||
>
|
||||
{{ $gettext('Enable') }}
|
||||
</n-button>
|
||||
</n-alert>
|
||||
<template v-else-if="!topSQL.enabled">
|
||||
<n-alert type="info">
|
||||
{{
|
||||
$gettext(
|
||||
'performance_schema is not enabled. After enabling, SQL performance statistics will be collected, which increases memory usage and requires a restart.',
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<n-flex>
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="enableTopSQLLoading"
|
||||
:disabled="enableTopSQLLoading"
|
||||
@click="handleEnableTopSQL"
|
||||
>
|
||||
{{ $gettext('Enable') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template v-if="topSQL.enabled">
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="() => refreshTopSQL()">
|
||||
|
||||
@@ -174,23 +174,25 @@ const handleResetTopSQL = async () => {
|
||||
$gettext('pg_stat_statements is configured, restart PostgreSQL to take effect.')
|
||||
}}
|
||||
</n-alert>
|
||||
<n-alert v-else-if="!topSQL.enabled" type="info">
|
||||
{{
|
||||
$gettext(
|
||||
'pg_stat_statements is not enabled. After enabling, SQL performance statistics will be collected, a restart of PostgreSQL is required.',
|
||||
)
|
||||
}}
|
||||
<n-button
|
||||
class="ml-16"
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="enableTopSQLLoading"
|
||||
:disabled="enableTopSQLLoading"
|
||||
@click="handleEnableTopSQL"
|
||||
>
|
||||
{{ $gettext('Enable') }}
|
||||
</n-button>
|
||||
</n-alert>
|
||||
<template v-else-if="!topSQL.enabled">
|
||||
<n-alert type="info">
|
||||
{{
|
||||
$gettext(
|
||||
'pg_stat_statements is not enabled. After enabling, SQL performance statistics will be collected, a restart of PostgreSQL is required.',
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<n-flex>
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="enableTopSQLLoading"
|
||||
:disabled="enableTopSQLLoading"
|
||||
@click="handleEnableTopSQL"
|
||||
>
|
||||
{{ $gettext('Enable') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template v-if="topSQL.enabled">
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="() => refreshTopSQL()">
|
||||
|
||||
@@ -51,6 +51,61 @@ const { data: load } = useRequest(php.load(slug), {
|
||||
const { data: modules } = useRequest(php.modules(slug), {
|
||||
initialData: [],
|
||||
})
|
||||
const { data: processes, send: refreshProcesses } = useRequest(php.processes(slug), {
|
||||
initialData: [],
|
||||
})
|
||||
const { data: opcache, send: refreshOpcache } = useRequest(php.opcache(slug), {
|
||||
initialData: { enabled: true },
|
||||
})
|
||||
const { data: composer, send: refreshComposer } = useRequest(php.composer(slug), {
|
||||
initialData: { installed: true, version: '', mirror: '' },
|
||||
})
|
||||
|
||||
const composerMirror = ref('')
|
||||
watch(
|
||||
() => composer.value?.mirror,
|
||||
(val) => {
|
||||
composerMirror.value = val ?? ''
|
||||
},
|
||||
)
|
||||
|
||||
const composerMirrorOptions = computed(() => [
|
||||
{ label: $gettext('Official'), value: '' },
|
||||
{ label: $gettext('Aliyun Mirror'), value: 'https://mirrors.aliyun.com/composer/' },
|
||||
{ label: $gettext('Tencent Mirror'), value: 'https://mirrors.tencent.com/composer/' },
|
||||
])
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (!bytes || bytes <= 0) return '0 B'
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
let i = 0
|
||||
while (bytes >= 1024 && i < units.length - 1) {
|
||||
bytes /= 1024
|
||||
i++
|
||||
}
|
||||
return `${Math.round(bytes * 10) / 10} ${units[i]}`
|
||||
}
|
||||
|
||||
const processColumns: any = [
|
||||
{ title: 'PID', key: 'pid', width: 90 },
|
||||
{ title: $gettext('State'), key: 'state', width: 130, ellipsis: { tooltip: true } },
|
||||
{ title: $gettext('Requests'), key: 'requests', width: 100 },
|
||||
{ title: $gettext('Method'), key: 'method', width: 90 },
|
||||
{ title: 'URI', key: 'uri', minWidth: 250, ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: $gettext('Duration (ms)'),
|
||||
key: 'request_duration',
|
||||
width: 130,
|
||||
render: (row: any) => Math.round(row.request_duration / 1000),
|
||||
},
|
||||
{
|
||||
title: $gettext('Memory'),
|
||||
key: 'last_request_memory',
|
||||
width: 110,
|
||||
render: (row: any) => formatBytes(row.last_request_memory),
|
||||
},
|
||||
{ title: $gettext('Script'), key: 'script', minWidth: 250, ellipsis: { tooltip: true } },
|
||||
]
|
||||
|
||||
const moduleColumns: any = [
|
||||
{
|
||||
@@ -181,6 +236,34 @@ const handleUninstallModule = async (module: string) => {
|
||||
window.$message.success($gettext('Task submitted, please check progress in background tasks'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleResetOpcache = async () => {
|
||||
const ok = await confirmAction({
|
||||
type: 'warning',
|
||||
title: $gettext('Confirm Reset'),
|
||||
content: $gettext(
|
||||
'Resetting will clear all cached scripts, performance may fluctuate briefly. Are you sure?',
|
||||
),
|
||||
})
|
||||
if (!ok) return
|
||||
useRequest(php.resetOpcache(slug)).onSuccess(() => {
|
||||
window.$message.success($gettext('Reset successfully'))
|
||||
refreshOpcache()
|
||||
})
|
||||
}
|
||||
|
||||
const handleInstallComposer = async () => {
|
||||
useRequest(php.installComposer(slug)).onSuccess(() => {
|
||||
window.$message.success($gettext('Task submitted, please check progress in background tasks'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveComposerMirror = async () => {
|
||||
useRequest(php.setComposerMirror(slug, composerMirror.value)).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
refreshComposer()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -257,14 +340,144 @@ const handleUninstallModule = async (module: string) => {
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" :tab="$gettext('Load Status')">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="400"
|
||||
:loading="false"
|
||||
:columns="loadColumns"
|
||||
:data="load"
|
||||
/>
|
||||
<n-flex vertical>
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="400"
|
||||
:loading="false"
|
||||
:columns="loadColumns"
|
||||
:data="load"
|
||||
/>
|
||||
<n-card :title="$gettext('FPM Processes')">
|
||||
<template #header-extra>
|
||||
<n-button size="small" type="primary" @click="() => refreshProcesses()">
|
||||
{{ $gettext('Refresh') }}
|
||||
</n-button>
|
||||
</template>
|
||||
<n-data-table
|
||||
striped
|
||||
:columns="processColumns"
|
||||
:data="processes"
|
||||
:scroll-x="1150"
|
||||
max-height="50vh"
|
||||
/>
|
||||
</n-card>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="opcache" tab="OPcache">
|
||||
<n-flex vertical>
|
||||
<n-alert v-if="!opcache.enabled" type="info">
|
||||
{{
|
||||
$gettext(
|
||||
'OPcache is not enabled. Install the Zend OPcache module in Module Management to significantly improve PHP performance.',
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<template v-else>
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="() => refreshOpcache()">
|
||||
{{ $gettext('Refresh') }}
|
||||
</n-button>
|
||||
<n-button type="warning" @click="handleResetOpcache">
|
||||
{{ $gettext('Reset OPcache') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-card :title="$gettext('Cache Statistics')">
|
||||
<n-flex>
|
||||
<n-statistic :label="$gettext('Hit Rate')" :value="`${opcache.hit_rate}%`" />
|
||||
<n-statistic class="ml-40" :label="$gettext('Hits')" :value="opcache.hits" />
|
||||
<n-statistic class="ml-40" :label="$gettext('Misses')" :value="opcache.misses" />
|
||||
<n-statistic
|
||||
class="ml-40"
|
||||
:label="$gettext('Cached Scripts')"
|
||||
:value="opcache.cached_scripts"
|
||||
/>
|
||||
<n-statistic
|
||||
class="ml-40"
|
||||
:label="$gettext('Cached Keys')"
|
||||
:value="`${opcache.cached_keys} / ${opcache.max_cached_keys}`"
|
||||
/>
|
||||
<n-statistic
|
||||
class="ml-40"
|
||||
:label="$gettext('OOM Restarts')"
|
||||
:value="opcache.oom_restarts"
|
||||
/>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
<n-card :title="$gettext('Memory')">
|
||||
<n-flex>
|
||||
<n-statistic :label="$gettext('Used')" :value="opcache.memory_used" />
|
||||
<n-statistic class="ml-40" :label="$gettext('Free')" :value="opcache.memory_free" />
|
||||
<n-statistic
|
||||
class="ml-40"
|
||||
:label="$gettext('Wasted')"
|
||||
:value="`${opcache.memory_wasted} (${opcache.wasted_percent}%)`"
|
||||
/>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
<n-card title="JIT">
|
||||
<n-flex v-if="opcache.jit_enabled">
|
||||
<n-statistic :label="$gettext('Buffer Size')" :value="opcache.jit_buffer_size" />
|
||||
<n-statistic
|
||||
class="ml-40"
|
||||
:label="$gettext('Buffer Free')"
|
||||
:value="opcache.jit_buffer_free"
|
||||
/>
|
||||
</n-flex>
|
||||
<n-text v-else depth="3">{{ $gettext('JIT is not enabled') }}</n-text>
|
||||
</n-card>
|
||||
</template>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="composer" tab="Composer">
|
||||
<n-flex vertical>
|
||||
<template v-if="!composer.installed">
|
||||
<n-alert type="info">
|
||||
{{ $gettext('Composer is not installed.') }}
|
||||
</n-alert>
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleInstallComposer">
|
||||
{{ $gettext('Install') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-card :title="$gettext('Composer')">
|
||||
<template #header-extra>
|
||||
<n-button size="small" type="primary" @click="handleInstallComposer">
|
||||
{{ $gettext('Update') }}
|
||||
</n-button>
|
||||
</template>
|
||||
<n-descriptions label-placement="left" :column="1">
|
||||
<n-descriptions-item :label="$gettext('Version')">
|
||||
{{ composer.version || '-' }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-card>
|
||||
<n-card :title="$gettext('Mirror')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="info">
|
||||
{{
|
||||
$gettext(
|
||||
'The mirror is a global setting shared by all PHP versions. Use a mirror to speed up package downloads in mainland China.',
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<n-flex>
|
||||
<n-select
|
||||
v-model:value="composerMirror"
|
||||
:options="composerMirrorOptions"
|
||||
class="w-80"
|
||||
/>
|
||||
<n-button type="primary" @click="handleSaveComposerMirror">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
</template>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="run-log" :tab="$gettext('Runtime Logs')">
|
||||
<realtime-log :service="'php-fpm-' + slug" />
|
||||
|
||||
Reference in New Issue
Block a user