mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-06 20:03:05 +08:00
- 新增 JMX/Endpoint/Agent 三种 JVM 连接模式与配置归一化链路 - 支持资源浏览、变更预览、写入应用、审计记录与只读约束 - 接入 AI 结构化写入计划与诊断计划回填能力 - 新增 Agent Bridge、Arthas Tunnel、JMX Helper 诊断传输实现 - 增加诊断控制台、命令模板、输出历史与自动补全交互 - 补齐前后端契约、运行夹具与 JVM 相关回归测试
295 lines
8.9 KiB
Go
295 lines
8.9 KiB
Go
package app
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"GoNavi-Wails/internal/connection"
|
||
"GoNavi-Wails/internal/jvm"
|
||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||
)
|
||
|
||
var newJVMDiagnosticTransport = jvm.NewDiagnosticTransport
|
||
|
||
const diagnosticChunkEvent = "jvm:diagnostic:chunk"
|
||
|
||
type diagnosticChunkEventPayload struct {
|
||
TabID string `json:"tabId"`
|
||
Chunk jvm.DiagnosticEventChunk `json:"chunk"`
|
||
}
|
||
|
||
func swapJVMDiagnosticTransportFactory(factory func(mode string) (jvm.DiagnosticTransport, error)) func() {
|
||
prev := newJVMDiagnosticTransport
|
||
newJVMDiagnosticTransport = factory
|
||
return func() { newJVMDiagnosticTransport = prev }
|
||
}
|
||
|
||
func resolveJVMDiagnosticTransport(cfg connection.ConnectionConfig) (connection.ConnectionConfig, jvm.DiagnosticTransport, error) {
|
||
normalized, err := jvm.NormalizeConnectionConfig(cfg)
|
||
if err != nil {
|
||
return connection.ConnectionConfig{}, nil, err
|
||
}
|
||
|
||
diagCfg, err := jvm.NormalizeDiagnosticConfig(normalized)
|
||
if err != nil {
|
||
return connection.ConnectionConfig{}, nil, err
|
||
}
|
||
if !diagCfg.Enabled {
|
||
return connection.ConnectionConfig{}, nil, errors.New("当前连接未启用 JVM 诊断增强模式")
|
||
}
|
||
normalized.JVM.Diagnostic = diagCfg
|
||
|
||
transport, err := newJVMDiagnosticTransport(diagCfg.Transport)
|
||
if err != nil {
|
||
return connection.ConnectionConfig{}, nil, err
|
||
}
|
||
return normalized, transport, nil
|
||
}
|
||
|
||
func (a *App) JVMProbeDiagnosticCapabilities(cfg connection.ConnectionConfig) connection.QueryResult {
|
||
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
items, err := transport.ProbeCapabilities(a.ctx, normalized)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
return connection.QueryResult{Success: true, Data: items}
|
||
}
|
||
|
||
func (a *App) JVMStartDiagnosticSession(cfg connection.ConnectionConfig, req jvm.DiagnosticSessionRequest) connection.QueryResult {
|
||
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
handle, err := transport.StartSession(a.ctx, normalized, req)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
return connection.QueryResult{Success: true, Data: handle}
|
||
}
|
||
|
||
func (a *App) JVMExecuteDiagnosticCommand(cfg connection.ConnectionConfig, tabID string, req jvm.DiagnosticCommandRequest) connection.QueryResult {
|
||
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
req.SessionID = strings.TrimSpace(req.SessionID)
|
||
req.CommandID = strings.TrimSpace(req.CommandID)
|
||
req.Command = strings.TrimSpace(req.Command)
|
||
req.Source = strings.TrimSpace(req.Source)
|
||
req.Reason = strings.TrimSpace(req.Reason)
|
||
|
||
if req.SessionID == "" {
|
||
return connection.QueryResult{Success: false, Message: "诊断会话 ID 不能为空,请先创建会话"}
|
||
}
|
||
if req.Command == "" {
|
||
return connection.QueryResult{Success: false, Message: "诊断命令不能为空"}
|
||
}
|
||
if req.CommandID == "" {
|
||
req.CommandID = fmt.Sprintf("diag-%d", time.Now().UnixNano())
|
||
}
|
||
if req.Source == "" {
|
||
req.Source = "manual"
|
||
}
|
||
|
||
commandType, err := jvm.ValidateDiagnosticCommandPolicy(normalized.JVM.Diagnostic, req.Command)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
riskLevel := diagnosticRiskLevel(commandType)
|
||
auditStore := jvm.NewDiagnosticAuditStore(filepath.Join(a.auditRootDir(), "jvm_diag_audit.jsonl"))
|
||
|
||
var auditWarnings []string
|
||
if err := auditStore.Append(jvm.DiagnosticAuditRecord{
|
||
ConnectionID: normalized.ID,
|
||
SessionID: req.SessionID,
|
||
CommandID: req.CommandID,
|
||
Transport: normalized.JVM.Diagnostic.Transport,
|
||
Command: req.Command,
|
||
CommandType: commandType,
|
||
Source: req.Source,
|
||
Reason: req.Reason,
|
||
RiskLevel: riskLevel,
|
||
Status: "running",
|
||
}); err != nil {
|
||
auditWarnings = append(auditWarnings, "审计记录写入失败: "+err.Error())
|
||
}
|
||
|
||
terminalSeen := false
|
||
appendTerminalAudit := func(status string) {
|
||
if terminalSeen {
|
||
return
|
||
}
|
||
terminalSeen = true
|
||
if err := auditStore.Append(jvm.DiagnosticAuditRecord{
|
||
ConnectionID: normalized.ID,
|
||
SessionID: req.SessionID,
|
||
CommandID: req.CommandID,
|
||
Transport: normalized.JVM.Diagnostic.Transport,
|
||
Command: req.Command,
|
||
CommandType: commandType,
|
||
Source: req.Source,
|
||
Reason: req.Reason,
|
||
RiskLevel: riskLevel,
|
||
Status: status,
|
||
}); err != nil {
|
||
auditWarnings = append(auditWarnings, "审计记录写入失败: "+err.Error())
|
||
}
|
||
}
|
||
|
||
if binder, ok := transport.(interface{ SetEventSink(jvm.DiagnosticEventSink) }); ok {
|
||
binder.SetEventSink(func(chunk jvm.DiagnosticEventChunk) {
|
||
if chunk.Timestamp == 0 {
|
||
chunk.Timestamp = time.Now().UnixMilli()
|
||
}
|
||
if strings.TrimSpace(chunk.SessionID) == "" {
|
||
chunk.SessionID = req.SessionID
|
||
}
|
||
if strings.TrimSpace(chunk.CommandID) == "" {
|
||
chunk.CommandID = req.CommandID
|
||
}
|
||
a.emitDiagnosticChunk(tabID, chunk)
|
||
if isDiagnosticTerminalPhase(chunk.Phase) {
|
||
appendTerminalAudit(chunk.Phase)
|
||
}
|
||
})
|
||
}
|
||
|
||
if err := transport.ExecuteCommand(a.ctx, normalized, req); err != nil {
|
||
phase := "failed"
|
||
if strings.Contains(strings.ToLower(err.Error()), "canceled") {
|
||
phase = "canceled"
|
||
}
|
||
if !terminalSeen {
|
||
chunk := jvm.DiagnosticEventChunk{
|
||
SessionID: req.SessionID,
|
||
CommandID: req.CommandID,
|
||
Event: "diagnostic",
|
||
Phase: phase,
|
||
Content: err.Error(),
|
||
Timestamp: time.Now().UnixMilli(),
|
||
}
|
||
a.emitDiagnosticChunk(tabID, chunk)
|
||
appendTerminalAudit(phase)
|
||
}
|
||
return connection.QueryResult{Success: false, Message: joinDiagnosticMessages(err.Error(), auditWarnings)}
|
||
}
|
||
|
||
if !terminalSeen {
|
||
chunk := jvm.DiagnosticEventChunk{
|
||
SessionID: req.SessionID,
|
||
CommandID: req.CommandID,
|
||
Event: "diagnostic",
|
||
Phase: "completed",
|
||
Content: "诊断命令执行完成",
|
||
Timestamp: time.Now().UnixMilli(),
|
||
}
|
||
a.emitDiagnosticChunk(tabID, chunk)
|
||
appendTerminalAudit("completed")
|
||
}
|
||
|
||
return connection.QueryResult{
|
||
Success: true,
|
||
Message: joinDiagnosticMessages("", auditWarnings),
|
||
Data: map[string]any{
|
||
"sessionId": req.SessionID,
|
||
"commandId": req.CommandID,
|
||
"status": "accepted",
|
||
},
|
||
}
|
||
}
|
||
|
||
func (a *App) JVMCancelDiagnosticCommand(cfg connection.ConnectionConfig, tabID string, sessionID string, commandID string) connection.QueryResult {
|
||
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
sessionID = strings.TrimSpace(sessionID)
|
||
commandID = strings.TrimSpace(commandID)
|
||
if sessionID == "" || commandID == "" {
|
||
return connection.QueryResult{Success: false, Message: "取消命令缺少 sessionId 或 commandId"}
|
||
}
|
||
|
||
if err := transport.CancelCommand(a.ctx, normalized, sessionID, commandID); err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
a.emitDiagnosticChunk(tabID, jvm.DiagnosticEventChunk{
|
||
SessionID: sessionID,
|
||
CommandID: commandID,
|
||
Event: "diagnostic",
|
||
Phase: "canceling",
|
||
Content: "已发送取消请求,等待诊断桥接端结束命令",
|
||
Timestamp: time.Now().UnixMilli(),
|
||
})
|
||
return connection.QueryResult{
|
||
Success: true,
|
||
Data: map[string]any{
|
||
"sessionId": sessionID,
|
||
"commandId": commandID,
|
||
"status": "cancel-requested",
|
||
},
|
||
}
|
||
}
|
||
|
||
func (a *App) JVMListDiagnosticAuditRecords(connectionID string, limit int) connection.QueryResult {
|
||
records, err := jvm.NewDiagnosticAuditStore(filepath.Join(a.auditRootDir(), "jvm_diag_audit.jsonl")).List(connectionID, limit)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
return connection.QueryResult{Success: true, Data: records}
|
||
}
|
||
|
||
func (a *App) emitDiagnosticChunk(tabID string, chunk jvm.DiagnosticEventChunk) {
|
||
if a.ctx == nil {
|
||
return
|
||
}
|
||
runtime.EventsEmit(a.ctx, diagnosticChunkEvent, diagnosticChunkEventPayload{
|
||
TabID: strings.TrimSpace(tabID),
|
||
Chunk: chunk,
|
||
})
|
||
}
|
||
|
||
func diagnosticRiskLevel(commandType string) string {
|
||
switch strings.TrimSpace(commandType) {
|
||
case jvm.DiagnosticCommandCategoryObserve:
|
||
return "low"
|
||
case jvm.DiagnosticCommandCategoryTrace:
|
||
return "medium"
|
||
default:
|
||
return "high"
|
||
}
|
||
}
|
||
|
||
func isDiagnosticTerminalPhase(phase string) bool {
|
||
switch strings.ToLower(strings.TrimSpace(phase)) {
|
||
case "completed", "failed", "canceled":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func joinDiagnosticMessages(primary string, warnings []string) string {
|
||
items := make([]string, 0, 1+len(warnings))
|
||
if strings.TrimSpace(primary) != "" {
|
||
items = append(items, strings.TrimSpace(primary))
|
||
}
|
||
for _, warning := range warnings {
|
||
if strings.TrimSpace(warning) == "" {
|
||
continue
|
||
}
|
||
items = append(items, strings.TrimSpace(warning))
|
||
}
|
||
return strings.Join(items, ";")
|
||
}
|