feat: 18 引擎驱动全家桶 + 驱动管理/离线包 + 设置中心 + 工作台交互打磨
CI / Go server (push) Canceled after 0s
CI / Web + core packages (push) Canceled after 0s
CI / Desktop (Tauri) cargo check (push) Canceled after 0s

驱动接入(全部本机真实例验收):
- Elasticsearch 原生 SQL API、InfluxDB 原生 HTTP+Flux(官方 JDBC 付费许可,实测弃用)
- ZooKeeper/MongoDB JVM agent(连接时自动下载官方驱动 jar,sha256 钉版本到 ~/.opsxdb/drivers/)
- 达梦 JDBC agent 桥就绪(待厂商 jar+实例);TiDB 真实例验收
- 协议族代码审查放行 PolarDB/TDSQL/OceanBase/Kingbase/GaussDB/Oracle
- 驱动能力模型:按驱动开关对象树分组/右键菜单/行编辑,NoSQL 树拍平(对象直挂库节点)

驱动管理:
- 驱动管理弹框(状态/预装/离线包导入/JDK 检测)+ /api/drivers/* 三接口
- opsxdb-driver 驱动商店仓库(清单/fetch.sh/pack.sh 离线包/import.sh)
- 连接时懒加载:DMG 零 Java 产物,首连自动下载(实测 ZK 九件套 1.35s),离线包自动回退

UI/交互:
- 设置中心(⚙):常规/执行/结果/驱动/快捷键/关于,左导航+右面板(对标 opsxshell)
- 实例级展开箭头(行首 SVG chevron,替代字体回退失真的 Unicode 三角)
- 树内嵌套渲染修复:对象树挂在激活连接下;工具栏窄窗口不断行(按钮 nowrap+整颗换行)
- 侧栏连接行精简(图标+连接名)、SQLite SSH 守卫、Oracle 死参数清理

质量:
- Go 协议层/离线回退单测、E2E 门控测试(ES/ZK/Influx/Mongo/TiDB/JDBC 桥)
- 驱动注册表单一事实来源 connection-types.json,README/BACKLOG/AGENTS 同步
This commit is contained in:
logan committed 2026-09-22 01:07:46 +08:00
1 parent 26f459d1b8
commit a9510235bb
156 files changed
+17200 -2520

No files matched your search

+394
View File
@@ -0,0 +1,394 @@
package db
// agent_test.go —— JDBC agent 桥的协议层测试。
// 用 helper process 扮演 JVM agent(TestMain 拦截 -agent-stub),
// 无需本机安装 Java 即可验证:握手、请求/响应路由、超时、进程退出、并发串行写。
import (
"archive/zip"
"bufio"
"context"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
"time"
)
// TestMain 拦截 stub 模式:测试二进制被当成 agent 子进程启动时直接进入协议循环。
func TestMain(m *testing.M) {
for _, a := range os.Args {
if a == "-agent-stub" {
runAgentStub()
return
}
}
os.Exit(m.Run())
}
// runAgentStub 扮演 OpsxJdbcAgent 的最小协议面(与 agents/jdbc/OpsxJdbcAgent.java 对应)。
func runAgentStub() {
out := bufio.NewWriter(os.Stdout)
announce := agentResponse{ID: 0, OK: true}
announce.Result = json.RawMessage(`{"agent":"opsx-agent-jdbc","version":"0.1.0","protocol":1}`)
write := func(r agentResponse) {
b, _ := json.Marshal(r)
_, _ = out.WriteString(string(b) + "\n")
out.Flush()
}
write(announce)
in := bufio.NewScanner(os.Stdin)
in.Buffer(make([]byte, 1024*1024), 1024*1024)
for in.Scan() {
var req agentRequest
if json.Unmarshal(in.Bytes(), &req) != nil {
continue
}
args := req.Args
fail := func(msg string) agentResponse { return agentResponse{ID: req.ID, OK: false, Error: msg} }
switch req.Op {
case "hello":
r := agentResponse{ID: req.ID, OK: true}
r.Result = json.RawMessage(`{"agent":"opsx-agent-jdbc","version":"0.1.0","protocol":1}`)
write(r)
case "open":
if args["url"] == "" || args["url"] == nil {
write(fail("missing url"))
break
}
r := agentResponse{ID: req.ID, OK: true}
r.Result = json.RawMessage(`{"handle":"h1"}`)
write(r)
case "ping":
write(agentResponse{ID: req.ID, OK: true})
case "query":
if strings.Contains(fmt.Sprintf("%v", args["sql"]), "SLOW") {
time.Sleep(2 * time.Second)
}
r := agentResponse{ID: req.ID, OK: true}
r.Result = json.RawMessage(`{"columns":["id","name"],"rows":[{"id":"1","name":"demo"}],"truncated":false}`)
write(r)
case "exec":
r := agentResponse{ID: req.ID, OK: true}
r.Result = json.RawMessage(`{"affectedRows":3}`)
write(r)
case "metaDatabases":
r := agentResponse{ID: req.ID, OK: true}
r.Result = json.RawMessage(`{"names":["MAIN","SYS"]}`)
write(r)
case "close":
write(agentResponse{ID: req.ID, OK: true})
case "shutdown":
write(agentResponse{ID: req.ID, OK: true})
os.Exit(0)
default:
write(fail("unknown op: " + req.Op))
}
}
os.Exit(0)
}
// TestAgentStubHelper 占位:stub 子进程以 -test.run=TestAgentStubHelper 启动时
// 不会走到这里(TestMain 已拦截),但名字必须存在以匹配 -test.run。
func TestAgentStubHelper(t *testing.T) {}
func startStubAgent(t *testing.T) *AgentClient {
t.Helper()
cmd := exec.Command(os.Args[0], "-test.run=TestAgentStubHelper", "--", "-agent-stub")
cli, err := StartAgentFromCmd(cmd, agentDriverSpecs["dameng"])
if err != nil {
t.Fatalf("StartAgentFromCmd: %v", err)
}
t.Cleanup(func() { cli.Shutdown(context.Background()) })
return cli
}
func TestAgentClientRoundTrip(t *testing.T) {
cli := startStubAgent(t)
ctx := context.Background()
// open → query → exec → metaDatabases → ping
handle := ""
{
var res struct {
Handle string `json:"handle"`
}
if err := cli.call(ctx, "open", map[string]any{"url": "jdbc:dm://127.0.0.1:5236", "user": "SYSDBA"}, &res); err != nil {
t.Fatalf("open: %v", err)
}
if res.Handle != "h1" {
t.Fatalf("handle = %q, want h1", res.Handle)
}
handle = res.Handle
}
{
var res struct {
Columns []string `json:"columns"`
Rows []map[string]interface{} `json:"rows"`
}
if err := cli.call(ctx, "query", map[string]any{"handle": handle, "sql": "SELECT 1", "maxRows": 100}, &res); err != nil {
t.Fatalf("query: %v", err)
}
if len(res.Columns) != 2 || res.Rows[0]["name"] != "demo" {
t.Fatalf("query result mismatch: %+v", res)
}
}
{
var res struct {
AffectedRows int64 `json:"affectedRows"`
}
if err := cli.call(ctx, "exec", map[string]any{"handle": handle, "sql": "UPDATE t SET a=1"}, &res); err != nil {
t.Fatalf("exec: %v", err)
}
if res.AffectedRows != 3 {
t.Fatalf("affectedRows = %d, want 3", res.AffectedRows)
}
}
{
var res struct {
Names []string `json:"names"`
}
if err := cli.call(ctx, "metaDatabases", map[string]any{"handle": handle}, &res); err != nil {
t.Fatalf("metaDatabases: %v", err)
}
if len(res.Names) != 2 || res.Names[0] != "MAIN" {
t.Fatalf("names = %v", res.Names)
}
}
if err := cli.call(ctx, "ping", map[string]any{"handle": handle}, nil); err != nil {
t.Fatalf("ping: %v", err)
}
}
func TestAgentCallTimeout(t *testing.T) {
cli := startStubAgent(t)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
var res map[string]any
err := cli.call(ctx, "query", map[string]any{"handle": "h1", "sql": "SELECT SLOW", "maxRows": 10}, &res)
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("err = %v, want DeadlineExceeded", err)
}
}
func TestAgentProcessExit(t *testing.T) {
cli := startStubAgent(t)
cli.Kill() // 杀进程后再调用,必须报进程退出而非挂死
var res map[string]any
err := cli.call(context.Background(), "ping", map[string]any{"handle": "h1"}, &res)
if err == nil {
t.Fatal("期望进程退出错误,got nil")
}
}
func TestAgentConcurrentCalls(t *testing.T) {
cli := startStubAgent(t)
ctx := context.Background()
if err := cli.call(ctx, "open", map[string]any{"url": "jdbc:dm://h:5236"}, nil); err != nil {
t.Fatalf("open: %v", err)
}
const n = 24
var wg sync.WaitGroup
errs := make([]error, n)
for i := 0; i < n; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
var res struct {
Columns []string `json:"columns"`
}
errs[i] = cli.call(ctx, "query", map[string]any{"handle": "h1", "sql": "SELECT 1", "maxRows": 1}, &res)
}(i)
}
wg.Wait()
for i, err := range errs {
if err != nil {
t.Fatalf("call %d: %v", i, err)
}
}
}
func TestRenderAgentURL(t *testing.T) {
spec := agentDriverSpecs["dameng"]
url, err := renderAgentURL(spec, ConnReq{Host: "127.0.0.1", Port: 5236})
if err != nil {
t.Fatalf("render: %v", err)
}
if url != "jdbc:dm://127.0.0.1:5236" {
t.Fatalf("url = %q", url)
}
if _, err := renderAgentURL(spec, ConnReq{Host: "", Port: 5236}); err == nil {
t.Fatal("空主机必须报错")
}
if _, err := renderAgentURL(spec, ConnReq{Host: "127.0.0.1", Port: 0}); err == nil {
t.Fatal("零端口必须报错")
}
}
func TestDownloadPinnedChecksum(t *testing.T) {
payload := []byte("fake-gson-bytes")
good := sha1.Sum(payload)
cases := []struct {
name string
body []byte
want string
wantErr bool
}{
{"校验通过", payload, hex.EncodeToString(good[:]), false},
{"校验失败拒绝落盘", []byte("tampered"), hex.EncodeToString(good[:]), true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(tc.body)
}))
defer srv.Close()
dest := t.TempDir() + "/gson.jar"
err := downloadPinned(srv.URL, dest, tc.want)
if tc.wantErr && err == nil {
t.Fatal("期望校验失败")
}
if !tc.wantErr {
if err != nil {
t.Fatalf("download: %v", err)
}
if _, statErr := os.Stat(dest); statErr != nil {
t.Fatalf("文件未落盘: %v", statErr)
}
} else {
if _, statErr := os.Stat(dest); statErr == nil {
t.Fatal("校验失败时文件不应存在")
}
}
})
}
}
func TestAgentConfigured(t *testing.T) {
dir := t.TempDir()
t.Setenv("OPSXDB_DAMENG_JARS", dir)
if ok, _ := AgentConfigured("dameng"); ok {
t.Fatal("空目录不应判为已配置")
}
if err := os.WriteFile(dir+"/DmJdbcDriver-8.1.3.140.jar", []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if ok, _ := AgentConfigured("dameng"); !ok {
t.Fatal("放置 jar 后应判为已配置")
}
if IsAgentDriver("mysql") {
t.Fatal("mysql 不应是 agent 驱动")
}
if !IsAgentDriver("dameng") {
t.Fatal("dameng 应是 agent 驱动")
}
}
// TestEnsureAgentRuntimeOfflinePack —— 网络不可用(镜像指向无效 + sha256 不匹配)时,
// ensureAgentRuntime 必须从离线驱动包导入 jar(pack.sh 产物)。
func TestEnsureAgentRuntimeOfflinePack(t *testing.T) {
home := t.TempDir()
t.Setenv("OPSXDB_HOME", home)
t.Setenv("OPSXDB_AGENT_JAR", filepath.Join(home, "agent", "opsx-agent-jdbc.jar"))
// 网络必失败:镜像指向本地空端口 + 错误的 sha256
t.Setenv("MAVEN_MIRROR", "http://127.0.0.1:1/none")
badSHA := sha1.Sum([]byte("bad"))
spec := AgentSpec{
DriverKey: "e2e-offline",
DriverClass: "x",
URLTemplate: "jdbc:x://{{.Host}}:{{.Port}}",
JarsDirEnv: "OPSXDB_OFFLINE_JARS",
}
agentDriverSpecs[spec.DriverKey] = spec
t.Cleanup(func() { delete(agentDriverSpecs, spec.DriverKey) })
agentDriverArtifacts[spec.DriverKey] = []agentArtifact{
{"demo.jar", "com.nope:demo:1.0", hex.EncodeToString(badSHA[:])},
}
// 构造离线包:manifest + 正确的 jar 内容(sha256 以包内 manifest 为准)
jarBytes := []byte("fake-jar-content")
files := map[string]string{"drivers/e2e-offline/demo.jar": string(jarBytes)}
manifestFiles := []map[string]string{{"path": "drivers/e2e-offline/demo.jar", "sha256": fmt.Sprintf("%x", sha256.Sum256(jarBytes))}}
zipPath := filepath.Join(home, "offline.zip")
if err := writeTestZip(zipPath, manifestFiles, files); err != nil {
t.Fatal(err)
}
t.Setenv("OPSXDB_OFFLINE_ZIP", zipPath)
// agent jar 也在包里(确保 ensureAgentJar 的离线路径不拦路)
if err := importFromOfflinePack(map[string]string{"pack/agent/opsx-agent-jdbc.jar": filepath.Join(home, "agent", "opsx-agent-jdbc.jar")}); err != nil {
// agent jar 不在此包属预期——单独构造一份最小 agent 条目
agentBytes := []byte("fake-agent")
mf2 := []map[string]string{{"path": "agent/opsx-agent-jdbc.jar", "sha256": fmt.Sprintf("%x", sha256.Sum256(agentBytes))}}
f2 := map[string]string{"agent/opsx-agent-jdbc.jar": string(agentBytes)}
if werr := writeTestZip(zipPath, append(manifestFiles, mf2...), mergeMaps(files, f2)); werr != nil {
t.Fatal(werr)
}
}
if err := ensureAgentRuntime(spec); err != nil {
t.Fatalf("ensureAgentRuntime 应走离线包成功: %v", err)
}
got, err := os.ReadFile(filepath.Join(home, "drivers", "e2e-offline", "demo.jar"))
if err != nil || string(got) != string(jarBytes) {
t.Fatalf("离线导入内容不符: %v", err)
}
// 负例:manifest hash 不符必须拒
jarBytes2 := []byte("tampered")
mf3 := []map[string]string{{"path": "drivers/e2e-offline/demo.jar", "sha256": fmt.Sprintf("%x", sha256.Sum256(jarBytes2))}}
if err := writeTestZip(zipPath, mf3, map[string]string{"drivers/e2e-offline/demo.jar": "not-the-same-content"}); err != nil {
t.Fatal(err)
}
os.Remove(filepath.Join(home, "drivers", "e2e-offline", "demo.jar"))
if err := ensureAgentRuntime(spec); err == nil {
t.Fatal("manifest 与实际内容不符时必须失败")
}
}
func writeTestZip(path string, manifestFiles []map[string]string, files map[string]string) error {
zf, err := os.Create(path)
if err != nil {
return err
}
defer zf.Close()
zw := zip.NewWriter(zf)
mf, err := zw.Create("pack/manifest.json")
if err != nil {
return err
}
b, _ := json.Marshal(map[string]any{"bundleVersion": 1, "files": manifestFiles})
if _, err := mf.Write(b); err != nil {
return err
}
for rel, content := range files {
w, err := zw.Create("pack/" + rel)
if err != nil {
return err
}
if _, err := w.Write([]byte(content)); err != nil {
return err
}
}
return zw.Close()
}
func mergeMaps(a, b map[string]string) map[string]string {
out := map[string]string{}
for k, v := range a {
out[k] = v
}
for k, v := range b {
out[k] = v
}
return out
}