package harness import ( "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "time" ) // Options 控制测试进程启动。 type Options struct { // Listen 写入配置的 listen,默认 127.0.0.1:0。 Listen string // AdminListen 可选;非空时写入 admin_listen,并读取 admin.addr。 AdminListen string // AdminInit 可选;nil 时用 CLIAdminInit。不支持时忽略并继续启动。 AdminInit AdminInitializer // KeepDir 为 true 时 Stop 不删除数据目录(命令行启动器在清理前可读)。 KeepDir bool } // Server 是一次集成测试用的真实 nixmsg 进程。 type Server struct { BinPath string ConfigPath string DataDir string Addr string // 来自 listen.addr,形如 127.0.0.1:12345 AdminAddr string // 来自 admin.addr(若有) AdminPassword string HTTPBase string AdminHTTPBase string cmd *exec.Cmd keepDir bool stopped bool } // Start 编译(如需)并启动服务:临时目录、listen 端口 0、读 listen.addr。 func Start(opts Options) (*Server, error) { bin, err := Binary() if err != nil { return nil, err } dataDir, err := os.MkdirTemp("", "nixmsg-harness-*") if err != nil { return nil, err } listen := opts.Listen if listen == "" { listen = "127.0.0.1:0" } cfgPath := filepath.Join(dataDir, "config.yaml") cfg := fmt.Sprintf("listen: %q\ndata_dir: %q\n", listen, filepath.ToSlash(dataDir)) if opts.AdminListen != "" { cfg += fmt.Sprintf("admin_listen: %q\n", opts.AdminListen) } if err = os.WriteFile(cfgPath, []byte(cfg), 0o644); err != nil { _ = os.RemoveAll(dataDir) return nil, err } adminInit := opts.AdminInit if adminInit == nil { adminInit = CLIAdminInit{} } password, initErr := adminInit.Init(bin, cfgPath) if initErr != nil && !errors.Is(initErr, ErrAdminInitUnsupported) { _ = os.RemoveAll(dataDir) return nil, initErr } if errors.Is(initErr, ErrAdminInitUnsupported) { password = "" } cmd := exec.Command(bin, "serve") cmd.Env = append(os.Environ(), "NIXMSG_CONFIG="+cfgPath) cmd.Stdout = os.Stderr cmd.Stderr = os.Stderr if err = cmd.Start(); err != nil { _ = os.RemoveAll(dataDir) return nil, fmt.Errorf("start serve: %w", err) } s := &Server{ BinPath: bin, ConfigPath: cfgPath, DataDir: dataDir, AdminPassword: password, cmd: cmd, keepDir: opts.KeepDir, } addr, err := waitAddrFile(filepath.Join(dataDir, "listen.addr"), 10*time.Second) if err != nil { _ = s.Stop() return nil, fmt.Errorf("wait listen.addr: %w", err) } s.Addr = addr s.HTTPBase = "http://" + addr if opts.AdminListen != "" { adminAddr, adminErr := waitAddrFile(filepath.Join(dataDir, "admin.addr"), 10*time.Second) if adminErr == nil { s.AdminAddr = adminAddr s.AdminHTTPBase = "http://" + adminAddr } } if s.AdminHTTPBase == "" { s.AdminHTTPBase = s.HTTPBase } return s, nil } // AdminClient 返回指向管理接口的 HTTP 客户端。 func (s *Server) AdminClient() (*AdminClient, error) { base := s.AdminHTTPBase if base == "" { base = s.HTTPBase } return NewAdminClient(base) } // Stop 结束进程并删除临时目录(除非 KeepDir)。 func (s *Server) Stop() error { if s == nil || s.stopped { return nil } s.stopped = true var stopErr error if s.cmd != nil && s.cmd.Process != nil { _ = s.cmd.Process.Kill() _, _ = s.cmd.Process.Wait() } if !s.keepDir && s.DataDir != "" { if err := os.RemoveAll(s.DataDir); err != nil { stopErr = err } } return stopErr } func waitAddrFile(path string, timeout time.Duration) (string, error) { deadline := time.Now().Add(timeout) var lastErr error for time.Now().Before(deadline) { b, err := os.ReadFile(path) if err == nil { addr := strings.TrimSpace(string(b)) if addr != "" { return addr, nil } lastErr = fmt.Errorf("empty addr file") } else { lastErr = err } time.Sleep(20 * time.Millisecond) } return "", lastErr }