mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-20 13:07:00 +08:00
Merge pull request #295 from Sxuan-Coder/feat/config-import-export
feat(config): 支持完整配置导入导出
This commit is contained in:
@@ -100,6 +100,16 @@ func (s *ProxyService) SaveUserConfig(cfg UserConfig) error {
|
||||
return s.core.SaveUserConfig(cfg)
|
||||
}
|
||||
|
||||
// ExportUserConfig 将当前完整配置导出为 YAML 文件。
|
||||
func (s *ProxyService) ExportUserConfig(path string) (string, error) {
|
||||
return s.core.ExportUserConfig(path)
|
||||
}
|
||||
|
||||
// ImportUserConfig 从 YAML 文件校验并替换当前完整配置。
|
||||
func (s *ProxyService) ImportUserConfig(path string) (UserConfig, error) {
|
||||
return s.core.ImportUserConfig(path)
|
||||
}
|
||||
|
||||
// GetCursorAccountStatus 返回 cursor-byok 独立 Cursor 账号的脱敏状态。
|
||||
func (s *ProxyService) GetCursorAccountStatus() CursorAccountStatus {
|
||||
return s.core.GetCursorAccountStatus()
|
||||
|
||||
@@ -17,6 +17,12 @@ func (s *ProxyService) LoadUserConfig() (UserConfig, error) {
|
||||
if s == nil {
|
||||
return serverconfig.DefaultConfig(), nil
|
||||
}
|
||||
s.configMu.Lock()
|
||||
defer s.configMu.Unlock()
|
||||
return s.loadUserConfig()
|
||||
}
|
||||
|
||||
func (s *ProxyService) loadUserConfig() (UserConfig, error) {
|
||||
app := application.Get()
|
||||
ctx := context.Background()
|
||||
if app != nil {
|
||||
@@ -36,6 +42,12 @@ func (s *ProxyService) SaveUserConfig(cfg UserConfig) error {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
s.configMu.Lock()
|
||||
defer s.configMu.Unlock()
|
||||
return s.saveUserConfig(cfg)
|
||||
}
|
||||
|
||||
func (s *ProxyService) saveUserConfig(cfg UserConfig) error {
|
||||
app := application.Get()
|
||||
ctx := context.Background()
|
||||
if app != nil {
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
serverconfig "cursor/internal/backend/server/config"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const maxConfigTransferFileSize = 4 << 20
|
||||
|
||||
type importedUserConfigDocument struct {
|
||||
serverconfig.Config `yaml:",inline"`
|
||||
LegacyRouting any `yaml:"routing,omitempty"`
|
||||
}
|
||||
|
||||
// ExportUserConfig 将当前完整配置导出为 YAML 文件。
|
||||
func (s *ProxyService) ExportUserConfig(path string) (string, error) {
|
||||
if s == nil {
|
||||
return "", errors.New("配置服务未初始化")
|
||||
}
|
||||
targetPath := normalizeConfigExportPath(path)
|
||||
if targetPath == "" {
|
||||
return "", errors.New("导出路径不能为空")
|
||||
}
|
||||
|
||||
s.configMu.Lock()
|
||||
defer s.configMu.Unlock()
|
||||
cfg, err := s.loadUserConfig()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取当前配置失败: %w", err)
|
||||
}
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("序列化导出配置失败: %w", err)
|
||||
}
|
||||
if err := writeExportedUserConfig(targetPath, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
// ImportUserConfig 从 YAML 文件校验并替换当前完整配置。
|
||||
func (s *ProxyService) ImportUserConfig(path string) (UserConfig, error) {
|
||||
if s == nil {
|
||||
return serverconfig.Config{}, errors.New("配置服务未初始化")
|
||||
}
|
||||
s.lifecycleMu.Lock()
|
||||
defer s.lifecycleMu.Unlock()
|
||||
s.configMu.Lock()
|
||||
defer s.configMu.Unlock()
|
||||
if err := ensureConfigImportAllowed(s.GetState()); err != nil {
|
||||
return serverconfig.Config{}, err
|
||||
}
|
||||
data, err := readImportedUserConfig(path)
|
||||
if err != nil {
|
||||
return serverconfig.Config{}, err
|
||||
}
|
||||
cfg, err := decodeImportedUserConfig(data)
|
||||
if err != nil {
|
||||
return serverconfig.Config{}, err
|
||||
}
|
||||
if err := s.saveUserConfig(cfg); err != nil {
|
||||
return serverconfig.Config{}, fmt.Errorf("保存导入配置失败: %w", err)
|
||||
}
|
||||
persisted, err := s.loadUserConfig()
|
||||
if err != nil {
|
||||
return serverconfig.Config{}, fmt.Errorf("重新读取导入配置失败: %w", err)
|
||||
}
|
||||
return persisted, nil
|
||||
}
|
||||
|
||||
func normalizeConfigExportPath(path string) string {
|
||||
trimmed := strings.TrimSpace(path)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
extension := strings.ToLower(filepath.Ext(trimmed))
|
||||
if extension != ".yaml" && extension != ".yml" {
|
||||
trimmed += ".yaml"
|
||||
}
|
||||
return filepath.Clean(trimmed)
|
||||
}
|
||||
|
||||
func writeExportedUserConfig(path string, data []byte) error {
|
||||
directory := filepath.Dir(path)
|
||||
file, err := os.CreateTemp(directory, ".cursor-byok-config-*.tmp")
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建导出配置临时文件失败: %w", err)
|
||||
}
|
||||
temporaryPath := file.Name()
|
||||
defer os.Remove(temporaryPath)
|
||||
if err := file.Chmod(0o600); err != nil {
|
||||
_ = file.Close()
|
||||
return fmt.Errorf("设置导出配置权限失败: %w", err)
|
||||
}
|
||||
if _, err := file.Write(data); err != nil {
|
||||
_ = file.Close()
|
||||
return fmt.Errorf("写入导出配置失败: %w", err)
|
||||
}
|
||||
if err := file.Sync(); err != nil {
|
||||
_ = file.Close()
|
||||
return fmt.Errorf("同步导出配置失败: %w", err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return fmt.Errorf("关闭导出配置失败: %w", err)
|
||||
}
|
||||
if err := replaceExportFile(temporaryPath, path); err != nil {
|
||||
return fmt.Errorf("替换导出配置失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureConfigImportAllowed(state ProxyState) error {
|
||||
if state.BackendRunning || state.ProxyRunning || state.Running {
|
||||
return errors.New("服务运行中不能导入完整配置,请先停止服务")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readImportedUserConfig(path string) ([]byte, error) {
|
||||
sourcePath := strings.TrimSpace(path)
|
||||
if sourcePath == "" {
|
||||
return nil, errors.New("导入路径不能为空")
|
||||
}
|
||||
file, err := os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开导入配置失败: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取导入配置信息失败: %w", err)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return nil, errors.New("导入配置必须是普通文件")
|
||||
}
|
||||
if info.Size() > maxConfigTransferFileSize {
|
||||
return nil, fmt.Errorf("导入配置不能超过 %d MiB", maxConfigTransferFileSize>>20)
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(file, maxConfigTransferFileSize+1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取导入配置失败: %w", err)
|
||||
}
|
||||
if len(data) > maxConfigTransferFileSize {
|
||||
return nil, fmt.Errorf("导入配置不能超过 %d MiB", maxConfigTransferFileSize>>20)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func decodeImportedUserConfig(data []byte) (serverconfig.Config, error) {
|
||||
if err := validateImportedUserConfigDocument(data); err != nil {
|
||||
return serverconfig.Config{}, err
|
||||
}
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(data))
|
||||
decoder.KnownFields(true)
|
||||
var document importedUserConfigDocument
|
||||
if err := decoder.Decode(&document); err != nil {
|
||||
return serverconfig.Config{}, fmt.Errorf("导入配置包含未知字段或无效 YAML: %w", err)
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); err == nil {
|
||||
return serverconfig.Config{}, errors.New("导入配置只能包含单个 YAML 文档")
|
||||
} else if !errors.Is(err, io.EOF) {
|
||||
return serverconfig.Config{}, fmt.Errorf("解析导入配置尾部失败: %w", err)
|
||||
}
|
||||
normalized, err := serverconfig.NormalizeConfig(document.Config)
|
||||
if err != nil {
|
||||
return serverconfig.Config{}, fmt.Errorf("导入配置校验失败: %w", err)
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func validateImportedUserConfigDocument(data []byte) error {
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(data))
|
||||
var document yaml.Node
|
||||
if err := decoder.Decode(&document); err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return errors.New("导入配置不能为空")
|
||||
}
|
||||
return fmt.Errorf("导入配置不是有效 YAML: %w", err)
|
||||
}
|
||||
if len(document.Content) != 1 || document.Content[0].Kind != yaml.MappingNode {
|
||||
return errors.New("导入配置顶层必须是 YAML 对象")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !windows
|
||||
|
||||
package client
|
||||
|
||||
import "os"
|
||||
|
||||
func replaceExportFile(sourcePath, targetPath string) error {
|
||||
return os.Rename(sourcePath, targetPath)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//go:build windows
|
||||
|
||||
package client
|
||||
|
||||
import "golang.org/x/sys/windows"
|
||||
|
||||
func replaceExportFile(sourcePath, targetPath string) error {
|
||||
source, err := windows.UTF16PtrFromString(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target, err := windows.UTF16PtrFromString(targetPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return windows.MoveFileEx(
|
||||
source,
|
||||
target,
|
||||
windows.MOVEFILE_REPLACE_EXISTING|windows.MOVEFILE_WRITE_THROUGH,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
serverconfig "cursor/internal/backend/server/config"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestExportAndImportUserConfigRoundTrip(t *testing.T) {
|
||||
source := newConfigTransferTestService(t)
|
||||
want := serverconfig.DefaultConfig()
|
||||
want.Log = true
|
||||
want.ModelAdapters = []serverconfig.ModelAdapterConfig{{
|
||||
DisplayName: "迁移模型",
|
||||
Type: "openai",
|
||||
BaseURL: "https://provider.example/v1",
|
||||
APIKey: "migration-secret",
|
||||
TooltipData: "迁移备注",
|
||||
ModelID: "model-a",
|
||||
ReasoningEffort: "medium",
|
||||
OpenAIEndpoint: "/v1/responses",
|
||||
}}
|
||||
if err := source.SaveUserConfig(want); err != nil {
|
||||
t.Fatalf("SaveUserConfig() error = %v", err)
|
||||
}
|
||||
|
||||
exportPath, err := source.ExportUserConfig(filepath.Join(t.TempDir(), "cursor-byok-backup"))
|
||||
if err != nil {
|
||||
t.Fatalf("ExportUserConfig() error = %v", err)
|
||||
}
|
||||
if filepath.Ext(exportPath) != ".yaml" {
|
||||
t.Fatalf("ExportUserConfig() path = %q, want .yaml extension", exportPath)
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
info, statErr := os.Stat(exportPath)
|
||||
if statErr != nil {
|
||||
t.Fatalf("Stat() error = %v", statErr)
|
||||
}
|
||||
if gotMode := info.Mode().Perm(); gotMode != 0o600 {
|
||||
t.Fatalf("export mode = %o, want 600", gotMode)
|
||||
}
|
||||
}
|
||||
|
||||
target := newConfigTransferTestService(t)
|
||||
got, err := target.ImportUserConfig(exportPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ImportUserConfig() error = %v", err)
|
||||
}
|
||||
if !got.Log || len(got.ModelAdapters) != 1 {
|
||||
t.Fatalf("ImportUserConfig() = %#v", got)
|
||||
}
|
||||
adapter := got.ModelAdapters[0]
|
||||
if adapter.DisplayName != "迁移模型" || adapter.APIKey != "migration-secret" || adapter.ModelID != "model-a" {
|
||||
t.Fatalf("imported adapter = %#v", adapter)
|
||||
}
|
||||
|
||||
persisted, err := target.LoadUserConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadUserConfig() error = %v", err)
|
||||
}
|
||||
if len(persisted.ModelAdapters) != 1 || persisted.ModelAdapters[0].APIKey != "migration-secret" {
|
||||
t.Fatalf("persisted config = %#v", persisted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteExportedUserConfigReplacesExistingFile(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
path := filepath.Join(directory, "config.yaml")
|
||||
if err := os.WriteFile(path, []byte("old"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
if err := writeExportedUserConfig(path, []byte("new")); err != nil {
|
||||
t.Fatalf("writeExportedUserConfig() error = %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile() error = %v", err)
|
||||
}
|
||||
if string(data) != "new" {
|
||||
t.Fatalf("exported content = %q, want new", data)
|
||||
}
|
||||
matches, err := filepath.Glob(filepath.Join(directory, ".cursor-byok-config-*.tmp"))
|
||||
if err != nil || len(matches) != 0 {
|
||||
t.Fatalf("temporary exports = %v, error = %v", matches, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureConfigImportAllowedRejectsRunningService(t *testing.T) {
|
||||
for _, state := range []ProxyState{
|
||||
{BackendRunning: true},
|
||||
{ProxyRunning: true},
|
||||
{Running: true},
|
||||
} {
|
||||
if err := ensureConfigImportAllowed(state); err == nil {
|
||||
t.Fatalf("ensureConfigImportAllowed(%+v) error = nil", state)
|
||||
}
|
||||
}
|
||||
if err := ensureConfigImportAllowed(ProxyState{}); err != nil {
|
||||
t.Fatalf("ensureConfigImportAllowed(stopped) error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportUserConfigWaitsForLifecycleTransition(t *testing.T) {
|
||||
service := newConfigTransferTestService(t)
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
if err := os.WriteFile(path, []byte("modelAdapters: []\n"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
service.lifecycleMu.Lock()
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := service.ImportUserConfig(path)
|
||||
done <- err
|
||||
}()
|
||||
select {
|
||||
case err := <-done:
|
||||
service.lifecycleMu.Unlock()
|
||||
t.Fatalf("ImportUserConfig() completed during lifecycle transition: %v", err)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
service.lifecycleMu.Unlock()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatalf("ImportUserConfig() error = %v", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("ImportUserConfig() did not resume after lifecycle transition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeImportedUserConfigRejectsNonMappingDocuments(t *testing.T) {
|
||||
for _, raw := range []string{"", "null\n", "[]\n", "value\n"} {
|
||||
if _, err := decodeImportedUserConfig([]byte(raw)); err == nil {
|
||||
t.Fatalf("decodeImportedUserConfig(%q) error = nil", raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeImportedUserConfigAcceptsEmptyReasoningEffort(t *testing.T) {
|
||||
raw := []byte("modelAdapters:\n - displayName: model\n type: openai\n baseURL: https://example.com/v1\n apiKey: secret\n tooltipData: migrated model\n modelID: model-a\n reasoningEffort: ''\n openAIEndpoint: /v1/responses\n")
|
||||
got, err := decodeImportedUserConfig(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("decodeImportedUserConfig() error = %v", err)
|
||||
}
|
||||
if got.ModelAdapters[0].ReasoningEffort != "" {
|
||||
t.Fatalf("reasoningEffort = %q, want empty", got.ModelAdapters[0].ReasoningEffort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportUserConfigRejectsUnknownFieldsWithoutOverwriting(t *testing.T) {
|
||||
service := newConfigTransferTestService(t)
|
||||
current := serverconfig.DefaultConfig()
|
||||
current.Log = true
|
||||
if err := service.SaveUserConfig(current); err != nil {
|
||||
t.Fatalf("SaveUserConfig() error = %v", err)
|
||||
}
|
||||
|
||||
path := filepath.Join(t.TempDir(), "unknown.yaml")
|
||||
if err := os.WriteFile(path, []byte("modelAdapters: []\nunknownSetting: true\n"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
if _, err := service.ImportUserConfig(path); err == nil || !strings.Contains(err.Error(), "未知字段") {
|
||||
t.Fatalf("ImportUserConfig() error = %v, want unknown field error", err)
|
||||
}
|
||||
|
||||
persisted, err := service.LoadUserConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadUserConfig() error = %v", err)
|
||||
}
|
||||
if !persisted.Log {
|
||||
t.Fatal("invalid import overwrote the existing config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportUserConfigRejectsMultipleDocuments(t *testing.T) {
|
||||
service := newConfigTransferTestService(t)
|
||||
path := filepath.Join(t.TempDir(), "multiple.yaml")
|
||||
content := "modelAdapters: []\n---\nmodelAdapters: []\n"
|
||||
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
if _, err := service.ImportUserConfig(path); err == nil || !strings.Contains(err.Error(), "单个 YAML 文档") {
|
||||
t.Fatalf("ImportUserConfig() error = %v, want multiple document error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportUserConfigRejectsOversizedFile(t *testing.T) {
|
||||
service := newConfigTransferTestService(t)
|
||||
path := filepath.Join(t.TempDir(), "oversized.yaml")
|
||||
content := make([]byte, maxConfigTransferFileSize+1)
|
||||
if err := os.WriteFile(path, content, 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
if _, err := service.ImportUserConfig(path); err == nil || !strings.Contains(err.Error(), "不能超过") {
|
||||
t.Fatalf("ImportUserConfig() error = %v, want size limit error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeImportedUserConfigNormalizesValues(t *testing.T) {
|
||||
raw := []byte("backendListenAddr: ' 127.0.0.1:12345 '\nproxyListenAddr: '127.0.0.1:12346'\nmodelAdapters: []\n")
|
||||
got, err := decodeImportedUserConfig(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("decodeImportedUserConfig() error = %v", err)
|
||||
}
|
||||
if got.BackendListenAddr != "127.0.0.1:12345" || got.ProxyListenAddr != "127.0.0.1:12346" {
|
||||
t.Fatalf("decodeImportedUserConfig() = %#v", got)
|
||||
}
|
||||
|
||||
encoded, err := yaml.Marshal(got)
|
||||
if err != nil {
|
||||
t.Fatalf("yaml.Marshal() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(string(encoded), "backendListenAddr: 127.0.0.1:12345") {
|
||||
t.Fatalf("encoded config = %s", encoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeImportedUserConfigAcceptsLegacyRouting(t *testing.T) {
|
||||
raw := []byte("modelAdapters: []\nrouting:\n strategy: legacy\n")
|
||||
got, err := decodeImportedUserConfig(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("decodeImportedUserConfig() error = %v", err)
|
||||
}
|
||||
if len(got.ModelAdapters) != 0 {
|
||||
t.Fatalf("decodeImportedUserConfig() = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func newConfigTransferTestService(t *testing.T) *ProxyService {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
return &ProxyService{
|
||||
store: serverconfig.NewStore(filepath.Join(root, "config.yaml"), filepath.Join(root, "logs")),
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,8 @@ type ProxyState struct {
|
||||
|
||||
// StartProxy 用于处理与 StartProxy 相关的逻辑。
|
||||
func (s *ProxyService) StartProxy() (ProxyState, error) {
|
||||
s.lifecycleMu.Lock()
|
||||
defer s.lifecycleMu.Unlock()
|
||||
logger.Infof("start service requested config_path=%s logs_root=%s", s.configPath, s.logsRoot)
|
||||
fail := func(step string, err error) (ProxyState, error) {
|
||||
logger.Errorf("start service failed step=%s err=%v", step, err)
|
||||
@@ -127,6 +129,8 @@ func (s *ProxyService) StartProxy() (ProxyState, error) {
|
||||
|
||||
// StopProxy 用于处理与 StopProxy 相关的逻辑。
|
||||
func (s *ProxyService) StopProxy() (ProxyState, error) {
|
||||
s.lifecycleMu.Lock()
|
||||
defer s.lifecycleMu.Unlock()
|
||||
logger.Infof("stop service requested")
|
||||
fail := func(step string, err error) (ProxyState, error) {
|
||||
logger.Errorf("stop service failed step=%s err=%v", step, err)
|
||||
|
||||
@@ -49,6 +49,8 @@ type ProxyService struct {
|
||||
|
||||
// configMu 表示当前声明中的 configMu。
|
||||
configMu sync.Mutex
|
||||
// lifecycleMu 串行化服务启停与完整配置导入,避免过渡状态下切换配置。
|
||||
lifecycleMu sync.Mutex
|
||||
// configPath 表示当前声明中的 configPath。
|
||||
configPath string
|
||||
// store 表示统一的配置存储。
|
||||
|
||||
Reference in New Issue
Block a user