feat(jvm): 增加连接测试与能力探测 API

- 新增 JVM provider 工厂与 JMX、Endpoint 骨架实现
- 暴露 TestJVMConnection 和 JVMProbeCapabilities 并统一 QueryResult 返回
- 刷新 Wails 绑定与 JVM 连接模型,补齐前后端方法签名
- 补充 App 编排测试与 provider 契约测试,避免假成功和静默成功
- 更新需求追踪,记录 Task 2 审查结论与验证证据
This commit is contained in:
Syngnat
2026-04-22 17:52:28 +08:00
parent 15b1ad24d1
commit 03a1506686
10 changed files with 746 additions and 17 deletions

View File

@@ -26,7 +26,7 @@
- [x] 阶段 2影响分析完成
- [x] 阶段 3方案设计完成已形成正式设计文档
- [x] 阶段 4实施计划完成已形成正式实施计划
- [ ] 阶段 5实现与自检进行中Task 1 已完成并通过回归)
- [ ] 阶段 5实现与自检进行中Task 1、Task 2 已完成并通过回归)
- [ ] 阶段 6评审与交付
- [ ] 阶段 7发布与观察
@@ -40,11 +40,12 @@
- 用户明确目标 Java 服务大概率不允许 `-javaagent` 或运行时动态 attach
- 已形成 JVM 缓存可视化编辑正式设计文档
- 已形成 JVM Connector MVP 正式实施计划文档
- - 已完成 Task 1JVM 共享契约与配置归一化
- 已完成 Task 1JVM 共享契约与配置归一化
- 已完成 Task 2Provider 注册、连接测试与能力探测 API
- 进行中:
- Task 2建立后端 Provider 注册与连接探测 API
- Task 3接入 JVM 连接表单与图标
- 待处理:
- Task 3+Guard/Audit/App/UI/AI 结构化计划等后续任务
- Task 4+只读资源浏览、Guard/AuditAI 结构化计划等后续任务
## 5. 风险与阻塞
- 风险:
@@ -73,12 +74,15 @@
- JVM Connector 正式设计文档自检
- JVM Connector 实施计划文档自检
- Task 1JVM 共享契约与配置归一化
- Task 2Provider 注册、连接测试与能力探测 API
- 结果:
- 已确认存在可复用的连接桥接与编辑器基础设施
- 已完成正式设计文档落盘与自检,未发现占位词和明显范围冲突
- 已完成正式实施计划落盘与自检,已补齐共享 DTO、provider factory 和审计落盘等关键实现细节
- 已完成 JVM 连接共享契约、默认只读/默认 JMX 归一化、前端配置收敛与补测
- Task 1 已完成规格审查与代码质量审查,结论均通过
- 已完成 JVM Provider 工厂、JMX/Endpoint provider 骨架、App 层连接测试与能力探测 API
- Task 2 已完成规格审查与代码质量审查,结论均通过
- 证据(日志/截图/链接):
- `cmd/optional-driver-agent/main.go`
- `internal/db/database.go`
@@ -99,7 +103,19 @@
- `cd frontend && npm test -- src/utils/jvmConnectionConfig.test.ts`
- `cd frontend && npm test -- --run`
- `cd frontend && npm run build`
- `internal/jvm/provider.go`
- `internal/jvm/jmx_provider.go`
- `internal/jvm/http_provider.go`
- `internal/jvm/provider_contract_test.go`
- `internal/app/methods_jvm.go`
- `internal/app/methods_jvm_test.go`
- `frontend/wailsjs/go/app/App.d.ts`
- `frontend/wailsjs/go/app/App.js`
- `frontend/wailsjs/go/models.ts`
- `go test ./internal/app -run 'Test(TestJVMConnection|JVMProbeCapabilities)' -count=1`
- `go test ./internal/jvm ./internal/app -count=1`
- `wails build -clean`
## 8. 下一步
- 下一步行动:进入 Task 2建立 JVM Provider 注册、连接测试与能力探测 API并在完成后生成/校验 Wails 绑定代码
- 下一步行动:进入 Task 3接入 JVM 连接表单、图标与展示文案,并在前端完成最小交互闭环
- 负责人Codex

View File

@@ -127,6 +127,10 @@ export function InstallLocalDriverPackage(arg1:string,arg2:string,arg3:string,ar
export function InstallUpdateAndRestart():Promise<connection.QueryResult>;
export function JVMProbeCapabilities(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
export function ListSQLDirectory(arg1:string):Promise<connection.QueryResult>;
export function LogWindowDiagnostic(arg1:string,arg2:string):Promise<void>;
export function MongoDiscoverMembers(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
@@ -149,8 +153,6 @@ export function OpenDriverDownloadDirectory(arg1:string):Promise<connection.Quer
export function OpenSQLFile():Promise<connection.QueryResult>;
export function ListSQLDirectory(arg1:string):Promise<connection.QueryResult>;
export function PreviewImportFile(arg1:string):Promise<connection.QueryResult>;
export function ReadSQLFile(arg1:string):Promise<connection.QueryResult>;
@@ -223,8 +225,6 @@ export function RetrySecurityUpdateCurrentRound(arg1:app.RetrySecurityUpdateRequ
export function SaveConnection(arg1:connection.SavedConnectionInput):Promise<connection.SavedConnectionView>;
export function SelectSQLDirectory(arg1:string):Promise<connection.QueryResult>;
export function SaveGlobalProxy(arg1:connection.SaveGlobalProxyInput):Promise<connection.GlobalProxyView>;
export function SelectDataRootDirectory(arg1:string):Promise<connection.QueryResult>;
@@ -237,6 +237,8 @@ export function SelectDriverPackageDirectory(arg1:string):Promise<connection.Que
export function SelectDriverPackageFile(arg1:string):Promise<connection.QueryResult>;
export function SelectSQLDirectory(arg1:string):Promise<connection.QueryResult>;
export function SelectSSHKeyFile(arg1:string):Promise<connection.QueryResult>;
export function SetMacNativeWindowControls(arg1:boolean):Promise<void>;
@@ -247,4 +249,6 @@ export function StartSecurityUpdate(arg1:app.StartSecurityUpdateRequest):Promise
export function TestConnection(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
export function TestJVMConnection(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
export function TruncateTables(arg1:connection.ConnectionConfig,arg2:string,arg3:Array<string>):Promise<connection.QueryResult>;

View File

@@ -246,6 +246,14 @@ export function InstallUpdateAndRestart() {
return window['go']['app']['App']['InstallUpdateAndRestart']();
}
export function JVMProbeCapabilities(arg1) {
return window['go']['app']['App']['JVMProbeCapabilities'](arg1);
}
export function ListSQLDirectory(arg1) {
return window['go']['app']['App']['ListSQLDirectory'](arg1);
}
export function LogWindowDiagnostic(arg1, arg2) {
return window['go']['app']['App']['LogWindowDiagnostic'](arg1, arg2);
}
@@ -290,10 +298,6 @@ export function OpenSQLFile() {
return window['go']['app']['App']['OpenSQLFile']();
}
export function ListSQLDirectory(arg1) {
return window['go']['app']['App']['ListSQLDirectory'](arg1);
}
export function PreviewImportFile(arg1) {
return window['go']['app']['App']['PreviewImportFile'](arg1);
}
@@ -438,10 +442,6 @@ export function SaveConnection(arg1) {
return window['go']['app']['App']['SaveConnection'](arg1);
}
export function SelectSQLDirectory(arg1) {
return window['go']['app']['App']['SelectSQLDirectory'](arg1);
}
export function SaveGlobalProxy(arg1) {
return window['go']['app']['App']['SaveGlobalProxy'](arg1);
}
@@ -466,6 +466,10 @@ export function SelectDriverPackageFile(arg1) {
return window['go']['app']['App']['SelectDriverPackageFile'](arg1);
}
export function SelectSQLDirectory(arg1) {
return window['go']['app']['App']['SelectSQLDirectory'](arg1);
}
export function SelectSSHKeyFile(arg1) {
return window['go']['app']['App']['SelectSSHKeyFile'](arg1);
}
@@ -486,6 +490,10 @@ export function TestConnection(arg1) {
return window['go']['app']['App']['TestConnection'](arg1);
}
export function TestJVMConnection(arg1) {
return window['go']['app']['App']['TestJVMConnection'](arg1);
}
export function TruncateTables(arg1, arg2, arg3) {
return window['go']['app']['App']['TruncateTables'](arg1, arg2, arg3);
}

View File

@@ -456,6 +456,86 @@ export namespace connection {
return a;
}
}
export class JVMEndpointConfig {
enabled?: boolean;
baseUrl?: string;
apiKey?: string;
timeoutSeconds?: number;
static createFrom(source: any = {}) {
return new JVMEndpointConfig(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.enabled = source["enabled"];
this.baseUrl = source["baseUrl"];
this.apiKey = source["apiKey"];
this.timeoutSeconds = source["timeoutSeconds"];
}
}
export class JVMJMXConfig {
enabled?: boolean;
host?: string;
port?: number;
username?: string;
password?: string;
domainAllowlist?: string[];
static createFrom(source: any = {}) {
return new JVMJMXConfig(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.enabled = source["enabled"];
this.host = source["host"];
this.port = source["port"];
this.username = source["username"];
this.password = source["password"];
this.domainAllowlist = source["domainAllowlist"];
}
}
export class JVMConfig {
environment?: string;
readOnly?: boolean;
allowedModes?: string[];
preferredMode?: string;
jmx?: JVMJMXConfig;
endpoint?: JVMEndpointConfig;
static createFrom(source: any = {}) {
return new JVMConfig(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.environment = source["environment"];
this.readOnly = source["readOnly"];
this.allowedModes = source["allowedModes"];
this.preferredMode = source["preferredMode"];
this.jmx = this.convertValues(source["jmx"], JVMJMXConfig);
this.endpoint = this.convertValues(source["endpoint"], JVMEndpointConfig);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class HTTPTunnelConfig {
host: string;
port: number;
@@ -549,6 +629,7 @@ export namespace connection {
mongoAuthMechanism?: string;
mongoReplicaUser?: string;
mongoReplicaPassword?: string;
jvm?: JVMConfig;
static createFrom(source: any = {}) {
return new ConnectionConfig(source);
@@ -590,6 +671,7 @@ export namespace connection {
this.mongoAuthMechanism = source["mongoAuthMechanism"];
this.mongoReplicaUser = source["mongoReplicaUser"];
this.mongoReplicaPassword = source["mongoReplicaPassword"];
this.jvm = this.convertValues(source["jvm"], JVMConfig);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
@@ -638,6 +720,9 @@ export namespace connection {
}
export class QueryResult {
success: boolean;
message: string;

View File

@@ -0,0 +1,60 @@
package app
import (
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/jvm"
)
var newJVMProvider = jvm.NewProvider
func (a *App) TestJVMConnection(cfg connection.ConnectionConfig) connection.QueryResult {
normalized, err := jvm.NormalizeConnectionConfig(cfg)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
provider, err := newJVMProvider(normalized.JVM.PreferredMode)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := provider.TestConnection(a.ctx, normalized); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Message: "JVM 连接成功"}
}
func (a *App) JVMProbeCapabilities(cfg connection.ConnectionConfig) connection.QueryResult {
normalized, err := jvm.NormalizeConnectionConfig(cfg)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
items := make([]jvm.Capability, 0, len(normalized.JVM.AllowedModes))
for _, mode := range normalized.JVM.AllowedModes {
provider, providerErr := newJVMProvider(mode)
if providerErr != nil {
items = append(items, jvm.Capability{
Mode: mode,
DisplayLabel: jvm.ModeDisplayLabel(mode),
Reason: providerErr.Error(),
})
continue
}
caps, probeErr := provider.ProbeCapabilities(a.ctx, normalized)
if probeErr != nil {
items = append(items, jvm.Capability{
Mode: mode,
DisplayLabel: jvm.ModeDisplayLabel(mode),
Reason: probeErr.Error(),
})
continue
}
items = append(items, caps...)
}
return connection.QueryResult{Success: true, Data: items}
}

View File

@@ -0,0 +1,240 @@
package app
import (
"context"
"errors"
"strings"
"testing"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/jvm"
)
type fakeJVMProvider struct {
testErr error
probe []jvm.Capability
probeErr error
list []jvm.ResourceSummary
value jvm.ValueSnapshot
apply jvm.ApplyResult
}
func (f fakeJVMProvider) Mode() string { return jvm.ModeJMX }
func (f fakeJVMProvider) TestConnection(context.Context, connection.ConnectionConfig) error {
return f.testErr
}
func (f fakeJVMProvider) ProbeCapabilities(context.Context, connection.ConnectionConfig) ([]jvm.Capability, error) {
return f.probe, f.probeErr
}
func (f fakeJVMProvider) ListResources(context.Context, connection.ConnectionConfig, string) ([]jvm.ResourceSummary, error) {
return f.list, nil
}
func (f fakeJVMProvider) GetValue(context.Context, connection.ConnectionConfig, string) (jvm.ValueSnapshot, error) {
return f.value, nil
}
func (f fakeJVMProvider) PreviewChange(context.Context, connection.ConnectionConfig, jvm.ChangeRequest) (jvm.ChangePreview, error) {
return jvm.ChangePreview{Allowed: true, Summary: "preview"}, nil
}
func (f fakeJVMProvider) ApplyChange(context.Context, connection.ConnectionConfig, jvm.ChangeRequest) (jvm.ApplyResult, error) {
return f.apply, nil
}
func swapJVMProviderFactory(factory func(mode string) (jvm.Provider, error)) func() {
prev := newJVMProvider
newJVMProvider = factory
return func() { newJVMProvider = prev }
}
func TestTestJVMConnectionUsesPreferredProvider(t *testing.T) {
app := NewAppWithSecretStore(nil)
var gotMode string
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
gotMode = mode
return fakeJVMProvider{}, nil
})
defer restore()
res := app.TestJVMConnection(connection.ConnectionConfig{
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
PreferredMode: "endpoint",
AllowedModes: []string{"jmx", "endpoint"},
},
})
if !res.Success {
t.Fatalf("expected success, got %+v", res)
}
if gotMode != "endpoint" {
t.Fatalf("expected provider mode endpoint, got %q", gotMode)
}
if res.Message != "JVM 连接成功" {
t.Fatalf("expected success message %q, got %q", "JVM 连接成功", res.Message)
}
}
func TestTestJVMConnectionReturnsProviderError(t *testing.T) {
app := NewAppWithSecretStore(nil)
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
return fakeJVMProvider{testErr: errors.New("dial failed")}, nil
})
defer restore()
res := app.TestJVMConnection(connection.ConnectionConfig{
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
PreferredMode: "jmx",
AllowedModes: []string{"jmx"},
},
})
if res.Success {
t.Fatalf("expected failure, got %+v", res)
}
if res.Message != "dial failed" {
t.Fatalf("expected message %q, got %q", "dial failed", res.Message)
}
}
func TestTestJVMConnectionReturnsProviderFactoryError(t *testing.T) {
app := NewAppWithSecretStore(nil)
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
return nil, errors.New("factory unavailable")
})
defer restore()
res := app.TestJVMConnection(connection.ConnectionConfig{
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
PreferredMode: "endpoint",
AllowedModes: []string{"endpoint"},
},
})
if res.Success {
t.Fatalf("expected failure, got %+v", res)
}
if res.Message != "factory unavailable" {
t.Fatalf("expected message %q, got %q", "factory unavailable", res.Message)
}
}
func TestJVMProbeCapabilitiesReturnsCapabilityArray(t *testing.T) {
app := NewAppWithSecretStore(nil)
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
return fakeJVMProvider{
probe: []jvm.Capability{{Mode: jvm.ModeJMX, CanBrowse: true, CanWrite: false, CanPreview: false, DisplayLabel: "JMX"}},
}, nil
})
defer restore()
res := app.JVMProbeCapabilities(connection.ConnectionConfig{
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
PreferredMode: "jmx",
AllowedModes: []string{"jmx"},
},
})
if !res.Success {
t.Fatalf("expected success, got %+v", res)
}
items, ok := res.Data.([]jvm.Capability)
if !ok || len(items) != 1 {
t.Fatalf("expected one capability, got %#v", res.Data)
}
}
func TestJVMProbeCapabilitiesIncludesReasonWhenProbeFails(t *testing.T) {
app := NewAppWithSecretStore(nil)
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
return fakeJVMProvider{
probeErr: errors.New("probe failed"),
}, nil
})
defer restore()
res := app.JVMProbeCapabilities(connection.ConnectionConfig{
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
PreferredMode: "jmx",
AllowedModes: []string{"jmx"},
},
})
if !res.Success {
t.Fatalf("expected success, got %+v", res)
}
items, ok := res.Data.([]jvm.Capability)
if !ok || len(items) != 1 {
t.Fatalf("expected one capability, got %#v", res.Data)
}
if items[0].Reason != "probe failed" {
t.Fatalf("expected reason %q, got %#v", "probe failed", items[0])
}
}
func TestJVMProbeCapabilitiesIncludesReasonWhenProviderFactoryFails(t *testing.T) {
app := NewAppWithSecretStore(nil)
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
return nil, errors.New("provider disabled")
})
defer restore()
res := app.JVMProbeCapabilities(connection.ConnectionConfig{
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
PreferredMode: "endpoint",
AllowedModes: []string{"endpoint"},
},
})
if !res.Success {
t.Fatalf("expected success, got %+v", res)
}
items, ok := res.Data.([]jvm.Capability)
if !ok || len(items) != 1 {
t.Fatalf("expected one capability, got %#v", res.Data)
}
if items[0].Reason != "provider disabled" {
t.Fatalf("expected reason %q, got %#v", "provider disabled", items[0])
}
if items[0].DisplayLabel != "Endpoint" {
t.Fatalf("expected display label %q, got %#v", "Endpoint", items[0])
}
}
func TestJVMProbeCapabilitiesUsesReadableLabelForUnsupportedMode(t *testing.T) {
app := NewAppWithSecretStore(nil)
restore := swapJVMProviderFactory(jvm.NewProvider)
defer restore()
res := app.JVMProbeCapabilities(connection.ConnectionConfig{
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
PreferredMode: "agent",
AllowedModes: []string{"agent"},
},
})
if !res.Success {
t.Fatalf("expected success, got %+v", res)
}
items, ok := res.Data.([]jvm.Capability)
if !ok || len(items) != 1 {
t.Fatalf("expected one capability, got %#v", res.Data)
}
if items[0].DisplayLabel != "Agent" {
t.Fatalf("expected display label %q, got %#v", "Agent", items[0])
}
if !strings.Contains(items[0].Reason, "unsupported jvm provider mode") {
t.Fatalf("expected unsupported mode error, got %#v", items[0])
}
}

View File

@@ -0,0 +1,93 @@
package jvm
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"GoNavi-Wails/internal/connection"
)
type HTTPProvider struct{}
func NewHTTPProvider() Provider { return &HTTPProvider{} }
func (p *HTTPProvider) Mode() string { return ModeEndpoint }
func (p *HTTPProvider) TestConnection(ctx context.Context, cfg connection.ConnectionConfig) error {
baseURL := strings.TrimSpace(cfg.JVM.Endpoint.BaseURL)
if baseURL == "" {
return fmt.Errorf("endpoint baseURL is required")
}
parsed, err := url.Parse(baseURL)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return fmt.Errorf("endpoint baseURL is invalid: %s", baseURL)
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return fmt.Errorf("endpoint scheme is unsupported: %s", parsed.Scheme)
}
timeout := time.Duration(cfg.JVM.Endpoint.TimeoutSeconds) * time.Second
if timeout <= 0 {
timeout = time.Duration(cfg.Timeout) * time.Second
}
if timeout <= 0 {
timeout = 5 * time.Second
}
client := &http.Client{Timeout: timeout}
resp, err := doEndpointProbe(ctx, client, baseURL, http.MethodHead)
if err != nil {
return err
}
if resp.StatusCode == http.StatusMethodNotAllowed || resp.StatusCode == http.StatusNotImplemented {
_ = resp.Body.Close()
resp, err = doEndpointProbe(ctx, client, baseURL, http.MethodGet)
if err != nil {
return err
}
}
defer resp.Body.Close()
if isReachableStatus(resp.StatusCode) {
return nil
}
return fmt.Errorf("endpoint returned unexpected status: %d", resp.StatusCode)
}
func (p *HTTPProvider) ProbeCapabilities(ctx context.Context, cfg connection.ConnectionConfig) ([]Capability, error) {
return []Capability{{Mode: ModeEndpoint, CanBrowse: true, CanWrite: true, CanPreview: true, DisplayLabel: "Endpoint"}}, nil
}
func (p *HTTPProvider) ListResources(ctx context.Context, cfg connection.ConnectionConfig, parentPath string) ([]ResourceSummary, error) {
return nil, errProviderNotImplemented(p.Mode(), "list resources")
}
func (p *HTTPProvider) GetValue(ctx context.Context, cfg connection.ConnectionConfig, resourcePath string) (ValueSnapshot, error) {
return ValueSnapshot{}, errProviderNotImplemented(p.Mode(), "get value")
}
func (p *HTTPProvider) PreviewChange(ctx context.Context, cfg connection.ConnectionConfig, req ChangeRequest) (ChangePreview, error) {
return ChangePreview{}, errProviderNotImplemented(p.Mode(), "preview change")
}
func (p *HTTPProvider) ApplyChange(ctx context.Context, cfg connection.ConnectionConfig, req ChangeRequest) (ApplyResult, error) {
return ApplyResult{}, errProviderNotImplemented(p.Mode(), "apply change")
}
func doEndpointProbe(ctx context.Context, client *http.Client, baseURL string, method string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, baseURL, nil)
if err != nil {
return nil, fmt.Errorf("endpoint request build failed: %w", err)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("endpoint request failed: %w", err)
}
return resp, nil
}
func isReachableStatus(statusCode int) bool {
return (statusCode >= 200 && statusCode < 400) || statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden
}

View File

@@ -0,0 +1,64 @@
package jvm
import (
"context"
"fmt"
"net"
"strconv"
"strings"
"time"
"GoNavi-Wails/internal/connection"
)
type JMXProvider struct{}
func NewJMXProvider() Provider { return &JMXProvider{} }
func (p *JMXProvider) Mode() string { return ModeJMX }
func (p *JMXProvider) TestConnection(ctx context.Context, cfg connection.ConnectionConfig) error {
host := strings.TrimSpace(cfg.JVM.JMX.Host)
if host == "" {
host = strings.TrimSpace(cfg.Host)
}
if host == "" {
return fmt.Errorf("jmx host is required")
}
port := cfg.JVM.JMX.Port
if port <= 0 {
return fmt.Errorf("jmx port is invalid: %d", port)
}
timeout := time.Duration(cfg.Timeout) * time.Second
if timeout <= 0 {
timeout = 5 * time.Second
}
dialer := net.Dialer{Timeout: timeout}
conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
if err != nil {
return fmt.Errorf("jmx tcp connect failed: %w", err)
}
_ = conn.Close()
return nil
}
func (p *JMXProvider) ProbeCapabilities(ctx context.Context, cfg connection.ConnectionConfig) ([]Capability, error) {
return []Capability{{Mode: ModeJMX, CanBrowse: true, CanWrite: false, CanPreview: false, DisplayLabel: "JMX"}}, nil
}
func (p *JMXProvider) ListResources(ctx context.Context, cfg connection.ConnectionConfig, parentPath string) ([]ResourceSummary, error) {
return nil, errProviderNotImplemented(p.Mode(), "list resources")
}
func (p *JMXProvider) GetValue(ctx context.Context, cfg connection.ConnectionConfig, resourcePath string) (ValueSnapshot, error) {
return ValueSnapshot{}, errProviderNotImplemented(p.Mode(), "get value")
}
func (p *JMXProvider) PreviewChange(ctx context.Context, cfg connection.ConnectionConfig, req ChangeRequest) (ChangePreview, error) {
return ChangePreview{}, errProviderNotImplemented(p.Mode(), "preview change")
}
func (p *JMXProvider) ApplyChange(ctx context.Context, cfg connection.ConnectionConfig, req ChangeRequest) (ApplyResult, error) {
return ApplyResult{}, errProviderNotImplemented(p.Mode(), "apply change")
}

57
internal/jvm/provider.go Normal file
View File

@@ -0,0 +1,57 @@
package jvm
import (
"context"
"fmt"
"strings"
"GoNavi-Wails/internal/connection"
)
type Provider interface {
Mode() string
TestConnection(ctx context.Context, cfg connection.ConnectionConfig) error
ProbeCapabilities(ctx context.Context, cfg connection.ConnectionConfig) ([]Capability, error)
ListResources(ctx context.Context, cfg connection.ConnectionConfig, parentPath string) ([]ResourceSummary, error)
GetValue(ctx context.Context, cfg connection.ConnectionConfig, resourcePath string) (ValueSnapshot, error)
PreviewChange(ctx context.Context, cfg connection.ConnectionConfig, req ChangeRequest) (ChangePreview, error)
ApplyChange(ctx context.Context, cfg connection.ConnectionConfig, req ChangeRequest) (ApplyResult, error)
}
var providerFactories = map[string]func() Provider{
ModeJMX: func() Provider { return NewJMXProvider() },
ModeEndpoint: func() Provider { return NewHTTPProvider() },
}
func NewProvider(mode string) (Provider, error) {
normalized := strings.ToLower(strings.TrimSpace(mode))
factory, ok := providerFactories[normalized]
if !ok {
return nil, fmt.Errorf("unsupported jvm provider mode: %s", mode)
}
return factory(), nil
}
func ModeDisplayLabel(mode string) string {
switch strings.ToLower(strings.TrimSpace(mode)) {
case ModeJMX:
return "JMX"
case ModeEndpoint:
return "Endpoint"
case ModeAgent:
return "Agent"
default:
normalized := strings.TrimSpace(mode)
if normalized == "" {
return "Unknown"
}
if len(normalized) == 1 {
return strings.ToUpper(normalized)
}
return strings.ToUpper(normalized[:1]) + strings.ToLower(normalized[1:])
}
}
func errProviderNotImplemented(mode string, action string) error {
return fmt.Errorf("%s provider does not implement %s yet", ModeDisplayLabel(mode), action)
}

View File

@@ -0,0 +1,102 @@
package jvm
import (
"context"
"strings"
"testing"
"GoNavi-Wails/internal/connection"
)
func TestJMXProviderTestConnectionReturnsErrorWhenHostMissing(t *testing.T) {
provider := NewJMXProvider()
err := provider.TestConnection(context.Background(), connection.ConnectionConfig{
Type: "jvm",
JVM: connection.JVMConfig{
JMX: connection.JVMJMXConfig{
Port: 9010,
},
},
})
if err == nil {
t.Fatal("expected error when jmx host is missing")
}
if !strings.Contains(err.Error(), "jmx host is required") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestJMXProviderTestConnectionReturnsErrorWhenPortInvalid(t *testing.T) {
provider := NewJMXProvider()
err := provider.TestConnection(context.Background(), connection.ConnectionConfig{
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
JMX: connection.JVMJMXConfig{
Port: 0,
},
},
})
if err == nil {
t.Fatal("expected error when jmx port is invalid")
}
if !strings.Contains(err.Error(), "jmx port is invalid") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestHTTPProviderTestConnectionReturnsErrorWhenBaseURLMissing(t *testing.T) {
provider := NewHTTPProvider()
err := provider.TestConnection(context.Background(), connection.ConnectionConfig{
Type: "jvm",
JVM: connection.JVMConfig{
Endpoint: connection.JVMEndpointConfig{
BaseURL: "",
},
},
})
if err == nil {
t.Fatal("expected error when endpoint baseURL is missing")
}
if !strings.Contains(err.Error(), "endpoint baseURL is required") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestHTTPProviderTestConnectionReturnsErrorWhenBaseURLInvalid(t *testing.T) {
provider := NewHTTPProvider()
err := provider.TestConnection(context.Background(), connection.ConnectionConfig{
Type: "jvm",
JVM: connection.JVMConfig{
Endpoint: connection.JVMEndpointConfig{
BaseURL: "://bad-url",
},
},
})
if err == nil {
t.Fatal("expected error when endpoint baseURL is invalid")
}
if !strings.Contains(err.Error(), "endpoint baseURL is invalid") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestJMXProviderListResourcesReturnsNotImplementedError(t *testing.T) {
provider := NewJMXProvider()
_, err := provider.ListResources(context.Background(), connection.ConnectionConfig{}, "")
if err == nil {
t.Fatal("expected not implemented error")
}
if !strings.Contains(strings.ToLower(err.Error()), "does not implement") {
t.Fatalf("unexpected error: %v", err)
}
}