Files
NixMsg/test/harness/server.go
T

163 lines
3.9 KiB
Go

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
}