Files
MyGoNavi/internal/db/optional_driver_agent_impl.go
Syngnat 26a7aacfec feat(drivers): 支持按需启动数据源并通过外置驱动代理减少发行包体积
- MySQL/Redis/Oracle/PostgreSQL 内置可用,其余数据源改为“安装启用”后可用
- 新建连接对未安装驱动做弹窗内拦截提示,并支持一键跳转驱动管理安装
- 驱动管理展示安装包真实大小(从 Release 资产元数据读取)并优化加载性能
- Release 工作流发布各平台驱动代理资产,主程序构建启用 -s -w 精简
2026-02-13 17:23:38 +08:00

441 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package db
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"sync"
"GoNavi-Wails/internal/connection"
)
const (
optionalAgentMethodConnect = "connect"
optionalAgentMethodClose = "close"
optionalAgentMethodPing = "ping"
optionalAgentMethodQuery = "query"
optionalAgentMethodExec = "exec"
optionalAgentMethodGetDatabases = "getDatabases"
optionalAgentMethodGetTables = "getTables"
optionalAgentMethodGetCreateStmt = "getCreateStatement"
optionalAgentMethodGetColumns = "getColumns"
optionalAgentMethodGetAllColumns = "getAllColumns"
optionalAgentMethodGetIndexes = "getIndexes"
optionalAgentMethodGetForeignKeys = "getForeignKeys"
optionalAgentMethodGetTriggers = "getTriggers"
optionalAgentMethodApplyChanges = "applyChanges"
optionalAgentDefaultScannerMaxBytes = 8 << 20
)
type optionalAgentRequest struct {
ID int64 `json:"id"`
Method string `json:"method"`
Config *connection.ConnectionConfig `json:"config,omitempty"`
Query string `json:"query,omitempty"`
DBName string `json:"dbName,omitempty"`
TableName string `json:"tableName,omitempty"`
Changes *connection.ChangeSet `json:"changes,omitempty"`
}
type optionalAgentResponse struct {
ID int64 `json:"id"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
Fields []string `json:"fields,omitempty"`
RowsAffected int64 `json:"rowsAffected,omitempty"`
}
type optionalDriverAgentClient struct {
cmd *exec.Cmd
stdin io.WriteCloser
reader *bufio.Reader
nextID int64
mu sync.Mutex
stderrMu sync.Mutex
stderr strings.Builder
driver string
}
func newOptionalDriverAgentClient(driverType string, executablePath string) (*optionalDriverAgentClient, error) {
pathText := strings.TrimSpace(executablePath)
if pathText == "" {
return nil, fmt.Errorf("%s 驱动代理路径为空", driverDisplayName(driverType))
}
if info, err := os.Stat(pathText); err != nil || info.IsDir() {
return nil, fmt.Errorf("%s 驱动代理不存在:%s", driverDisplayName(driverType), pathText)
}
cmd := exec.Command(pathText)
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, fmt.Errorf("创建 %s 驱动代理 stdin 失败:%w", driverDisplayName(driverType), err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("创建 %s 驱动代理 stdout 失败:%w", driverDisplayName(driverType), err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, fmt.Errorf("创建 %s 驱动代理 stderr 失败:%w", driverDisplayName(driverType), err)
}
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("启动 %s 驱动代理失败:%w", driverDisplayName(driverType), err)
}
client := &optionalDriverAgentClient{
cmd: cmd,
stdin: stdin,
reader: bufio.NewReader(stdout),
driver: normalizeRuntimeDriverType(driverType),
}
go client.captureStderr(stderr)
return client, nil
}
func (c *optionalDriverAgentClient) captureStderr(stderr io.Reader) {
scanner := bufio.NewScanner(stderr)
buffer := make([]byte, 0, 8<<10)
scanner.Buffer(buffer, optionalAgentDefaultScannerMaxBytes)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
c.stderrMu.Lock()
if c.stderr.Len() > 0 {
c.stderr.WriteString(" | ")
}
c.stderr.WriteString(line)
c.stderrMu.Unlock()
}
}
func (c *optionalDriverAgentClient) stderrText() string {
c.stderrMu.Lock()
defer c.stderrMu.Unlock()
return strings.TrimSpace(c.stderr.String())
}
func (c *optionalDriverAgentClient) call(req optionalAgentRequest, out interface{}, fields *[]string, rowsAffected *int64) error {
c.mu.Lock()
defer c.mu.Unlock()
c.nextID++
req.ID = c.nextID
payload, err := json.Marshal(req)
if err != nil {
return err
}
payload = append(payload, '\n')
if _, err := c.stdin.Write(payload); err != nil {
stderrText := c.stderrText()
if stderrText == "" {
return fmt.Errorf("调用 %s 驱动代理失败:%w", driverDisplayName(c.driver), err)
}
return fmt.Errorf("调用 %s 驱动代理失败:%wstderr: %s", driverDisplayName(c.driver), err, stderrText)
}
line, err := c.reader.ReadBytes('\n')
if err != nil {
stderrText := c.stderrText()
if stderrText == "" {
return fmt.Errorf("读取 %s 驱动代理响应失败:%w", driverDisplayName(c.driver), err)
}
return fmt.Errorf("读取 %s 驱动代理响应失败:%wstderr: %s", driverDisplayName(c.driver), err, stderrText)
}
var resp optionalAgentResponse
if err := json.Unmarshal(line, &resp); err != nil {
return fmt.Errorf("解析 %s 驱动代理响应失败:%w", driverDisplayName(c.driver), err)
}
if !resp.Success {
errText := strings.TrimSpace(resp.Error)
if errText == "" {
errText = fmt.Sprintf("%s 驱动代理返回失败", driverDisplayName(c.driver))
}
return errors.New(errText)
}
if fields != nil {
*fields = resp.Fields
}
if rowsAffected != nil {
*rowsAffected = resp.RowsAffected
}
if out != nil && len(resp.Data) > 0 {
if err := json.Unmarshal(resp.Data, out); err != nil {
return fmt.Errorf("解析 %s 驱动代理数据失败:%w", driverDisplayName(c.driver), err)
}
}
return nil
}
func (c *optionalDriverAgentClient) close() error {
c.mu.Lock()
defer c.mu.Unlock()
var closeErr error
if c.stdin != nil {
_ = c.stdin.Close()
}
if c.cmd != nil && c.cmd.Process != nil {
if err := c.cmd.Process.Kill(); err != nil {
closeErr = err
}
}
if c.cmd != nil {
_ = c.cmd.Wait()
}
return closeErr
}
type OptionalDriverAgentDB struct {
driverType string
client *optionalDriverAgentClient
}
func newOptionalDriverAgentDatabase(driverType string) databaseFactory {
normalized := normalizeRuntimeDriverType(driverType)
return func() Database {
return &OptionalDriverAgentDB{driverType: normalized}
}
}
func (d *OptionalDriverAgentDB) Connect(config connection.ConnectionConfig) error {
if d.client != nil {
_ = d.client.close()
d.client = nil
}
executablePath, err := ResolveOptionalDriverAgentExecutablePath("", d.driverType)
if err != nil {
return err
}
client, err := newOptionalDriverAgentClient(d.driverType, executablePath)
if err != nil {
return err
}
if err := client.call(optionalAgentRequest{
Method: optionalAgentMethodConnect,
Config: &config,
}, nil, nil, nil); err != nil {
_ = client.close()
return err
}
d.client = client
return nil
}
func (d *OptionalDriverAgentDB) Close() error {
if d.client == nil {
return nil
}
_ = d.client.call(optionalAgentRequest{Method: optionalAgentMethodClose}, nil, nil, nil)
err := d.client.close()
d.client = nil
return err
}
func (d *OptionalDriverAgentDB) Ping() error {
client, err := d.requireClient()
if err != nil {
return err
}
return client.call(optionalAgentRequest{Method: optionalAgentMethodPing}, nil, nil, nil)
}
func (d *OptionalDriverAgentDB) QueryContext(ctx context.Context, query string) ([]map[string]interface{}, []string, error) {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
return d.Query(query)
}
func (d *OptionalDriverAgentDB) Query(query string) ([]map[string]interface{}, []string, error) {
client, err := d.requireClient()
if err != nil {
return nil, nil, err
}
var data []map[string]interface{}
var fields []string
if err := client.call(optionalAgentRequest{
Method: optionalAgentMethodQuery,
Query: query,
}, &data, &fields, nil); err != nil {
return nil, nil, err
}
return data, fields, nil
}
func (d *OptionalDriverAgentDB) ExecContext(ctx context.Context, query string) (int64, error) {
if err := ctx.Err(); err != nil {
return 0, err
}
return d.Exec(query)
}
func (d *OptionalDriverAgentDB) Exec(query string) (int64, error) {
client, err := d.requireClient()
if err != nil {
return 0, err
}
var affected int64
if err := client.call(optionalAgentRequest{
Method: optionalAgentMethodExec,
Query: query,
}, nil, nil, &affected); err != nil {
return 0, err
}
return affected, nil
}
func (d *OptionalDriverAgentDB) GetDatabases() ([]string, error) {
client, err := d.requireClient()
if err != nil {
return nil, err
}
var dbs []string
if err := client.call(optionalAgentRequest{
Method: optionalAgentMethodGetDatabases,
}, &dbs, nil, nil); err != nil {
return nil, err
}
return dbs, nil
}
func (d *OptionalDriverAgentDB) GetTables(dbName string) ([]string, error) {
client, err := d.requireClient()
if err != nil {
return nil, err
}
var tables []string
if err := client.call(optionalAgentRequest{
Method: optionalAgentMethodGetTables,
DBName: dbName,
}, &tables, nil, nil); err != nil {
return nil, err
}
return tables, nil
}
func (d *OptionalDriverAgentDB) GetCreateStatement(dbName, tableName string) (string, error) {
client, err := d.requireClient()
if err != nil {
return "", err
}
var sqlText string
if err := client.call(optionalAgentRequest{
Method: optionalAgentMethodGetCreateStmt,
DBName: dbName,
TableName: tableName,
}, &sqlText, nil, nil); err != nil {
return "", err
}
return sqlText, nil
}
func (d *OptionalDriverAgentDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) {
client, err := d.requireClient()
if err != nil {
return nil, err
}
var columns []connection.ColumnDefinition
if err := client.call(optionalAgentRequest{
Method: optionalAgentMethodGetColumns,
DBName: dbName,
TableName: tableName,
}, &columns, nil, nil); err != nil {
return nil, err
}
return columns, nil
}
func (d *OptionalDriverAgentDB) GetAllColumns(dbName string) ([]connection.ColumnDefinitionWithTable, error) {
client, err := d.requireClient()
if err != nil {
return nil, err
}
var columns []connection.ColumnDefinitionWithTable
if err := client.call(optionalAgentRequest{
Method: optionalAgentMethodGetAllColumns,
DBName: dbName,
}, &columns, nil, nil); err != nil {
return nil, err
}
return columns, nil
}
func (d *OptionalDriverAgentDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) {
client, err := d.requireClient()
if err != nil {
return nil, err
}
var indexes []connection.IndexDefinition
if err := client.call(optionalAgentRequest{
Method: optionalAgentMethodGetIndexes,
DBName: dbName,
TableName: tableName,
}, &indexes, nil, nil); err != nil {
return nil, err
}
return indexes, nil
}
func (d *OptionalDriverAgentDB) GetForeignKeys(dbName, tableName string) ([]connection.ForeignKeyDefinition, error) {
client, err := d.requireClient()
if err != nil {
return nil, err
}
var keys []connection.ForeignKeyDefinition
if err := client.call(optionalAgentRequest{
Method: optionalAgentMethodGetForeignKeys,
DBName: dbName,
TableName: tableName,
}, &keys, nil, nil); err != nil {
return nil, err
}
return keys, nil
}
func (d *OptionalDriverAgentDB) GetTriggers(dbName, tableName string) ([]connection.TriggerDefinition, error) {
client, err := d.requireClient()
if err != nil {
return nil, err
}
var triggers []connection.TriggerDefinition
if err := client.call(optionalAgentRequest{
Method: optionalAgentMethodGetTriggers,
DBName: dbName,
TableName: tableName,
}, &triggers, nil, nil); err != nil {
return nil, err
}
return triggers, nil
}
func (d *OptionalDriverAgentDB) ApplyChanges(tableName string, changes connection.ChangeSet) error {
client, err := d.requireClient()
if err != nil {
return err
}
return client.call(optionalAgentRequest{
Method: optionalAgentMethodApplyChanges,
TableName: tableName,
Changes: &changes,
}, nil, nil, nil)
}
func (d *OptionalDriverAgentDB) requireClient() (*optionalDriverAgentClient, error) {
if d.client == nil {
return nil, fmt.Errorf("connection not open")
}
return d.client, nil
}