222 lines
6.6 KiB
Go
222 lines
6.6 KiB
Go
package instance
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestAcquireSeparatesConcurrentInstances 验证槽位分配:第一个进程用默认数据目录,
|
|
// 第二个进程拿到 instance-2,锁释放后默认实例可被复用。
|
|
func TestAcquireSeparatesConcurrentInstances(t *testing.T) {
|
|
baseDir := t.TempDir()
|
|
t.Setenv("HUYA_DATA_DIR", baseDir)
|
|
t.Setenv("HUYA_LOG_DIR", "")
|
|
|
|
first, err := Acquire()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer first.Release()
|
|
if first.Name() != "默认实例" || first.ID() != "default" {
|
|
t.Fatalf("first instance = %q/%q", first.Name(), first.ID())
|
|
}
|
|
if first.Paths().DataDir != baseDir {
|
|
t.Fatalf("first dataDir = %q, want %q", first.Paths().DataDir, baseDir)
|
|
}
|
|
if first.Paths().LogDir != filepath.Join(baseDir, "logs") {
|
|
t.Fatalf("first logDir = %q", first.Paths().LogDir)
|
|
}
|
|
|
|
second, err := Acquire()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer second.Release()
|
|
wantSecondDir := filepath.Join(baseDir, "instances", "instance-2")
|
|
if second.Name() != "实例 2" || second.ID() != "instance-2" {
|
|
t.Fatalf("second instance = %q/%q", second.Name(), second.ID())
|
|
}
|
|
if second.Paths().DataDir != wantSecondDir {
|
|
t.Fatalf("second dataDir = %q, want %q", second.Paths().DataDir, wantSecondDir)
|
|
}
|
|
// 日志目录必须落在实例自己的目录里,否则多开时日志会交错。
|
|
if second.Paths().LogDir != filepath.Join(wantSecondDir, "logs") {
|
|
t.Fatalf("second logDir = %q", second.Paths().LogDir)
|
|
}
|
|
|
|
first.Release()
|
|
reused, err := Acquire()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer reused.Release()
|
|
if reused.Name() != "默认实例" || reused.Paths().DataDir != baseDir {
|
|
t.Fatalf("reused instance = %q/%q", reused.Name(), reused.Paths().DataDir)
|
|
}
|
|
}
|
|
|
|
// TestWebviewDirsAreIsolated WebView2 一个用户数据目录只能有一个浏览器进程,
|
|
// 每个实例必须是不同目录,否则第二个实例会偶发开不出窗口。
|
|
func TestWebviewDirsAreIsolated(t *testing.T) {
|
|
baseDir := t.TempDir()
|
|
t.Setenv("HUYA_DATA_DIR", baseDir)
|
|
t.Setenv("HUYA_LOG_DIR", "")
|
|
|
|
first, err := Acquire()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer first.Release()
|
|
second, err := Acquire()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer second.Release()
|
|
|
|
firstDir := first.WebviewUserDataDir()
|
|
secondDir := second.WebviewUserDataDir()
|
|
if firstDir == "" || secondDir == "" || firstDir == secondDir {
|
|
t.Fatalf("webview dirs = %q / %q", firstDir, secondDir)
|
|
}
|
|
if want := filepath.Join(baseDir, "webview2"); firstDir != want {
|
|
t.Fatalf("first webview dir = %q, want %q", firstDir, want)
|
|
}
|
|
if want := filepath.Join(baseDir, "instances", "instance-2", "webview2"); secondDir != want {
|
|
t.Fatalf("second webview dir = %q, want %q", secondDir, want)
|
|
}
|
|
}
|
|
|
|
// TestAcquireNeverSharesDefaultDir 保证实例占位失败时也不会退回默认目录。
|
|
// 退回默认目录会让两个进程共享日志文件与前端数据目录。
|
|
func TestAcquireNeverSharesDefaultDir(t *testing.T) {
|
|
baseDir := t.TempDir()
|
|
// 用一个普通文件占住 <dataDir>,让 <dataDir>/instances/.locks 无法创建。
|
|
blocked := filepath.Join(baseDir, "blocked")
|
|
if err := os.WriteFile(blocked, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("HUYA_DATA_DIR", blocked)
|
|
t.Setenv("HUYA_LOG_DIR", "")
|
|
|
|
lease, err := Acquire()
|
|
if lease == nil {
|
|
t.Fatal("实例占位必须始终返回可用目录")
|
|
}
|
|
defer lease.Release()
|
|
if err == nil {
|
|
t.Fatal("降级路径应当把原因返回给上层记录")
|
|
}
|
|
if lease.Name() != "临时实例" {
|
|
t.Fatalf("降级实例名 = %q, want 临时实例", lease.Name())
|
|
}
|
|
if lease.Paths().DataDir == blocked || !strings.Contains(lease.ID(), "instance-tmp-") {
|
|
t.Fatalf("降级目录必须与本进程绑定且独立: %q", lease.Paths().DataDir)
|
|
}
|
|
}
|
|
|
|
// TestReleaseIsIdempotent 验证重复释放不会出错(Shutdown 与 defer 可能都调用一次)。
|
|
func TestReleaseIsIdempotent(t *testing.T) {
|
|
t.Setenv("HUYA_DATA_DIR", t.TempDir())
|
|
t.Setenv("HUYA_LOG_DIR", "")
|
|
|
|
lease, err := Acquire()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lease.Release()
|
|
lease.Release()
|
|
}
|
|
|
|
// TestAcquireIsExclusiveAcrossProcesses 用两个真实进程验证「多开」:第二个进程不会拿到
|
|
// 与第一个进程相同的数据目录(否则会互相覆盖日志与前端数据)。
|
|
func TestAcquireIsExclusiveAcrossProcesses(t *testing.T) {
|
|
if os.Getenv("HUYA_INSTANCE_HELPER") == "1" {
|
|
runInstanceHelper()
|
|
return
|
|
}
|
|
|
|
baseDir := t.TempDir()
|
|
first := startInstanceHelper(t, baseDir)
|
|
defer first.stop()
|
|
firstName, firstID, firstDir := first.read(t)
|
|
|
|
// 第一个进程已经拿到锁之后再启动第二个,避免出现「同时抢同一槽位」的随机性。
|
|
second := startInstanceHelper(t, baseDir)
|
|
defer second.stop()
|
|
_, secondID, secondDir := second.read(t)
|
|
|
|
if firstID != "default" || firstName != "默认实例" {
|
|
t.Fatalf("第一个进程实例 = %q/%q, want 默认实例/default", firstName, firstID)
|
|
}
|
|
if secondID != "instance-2" {
|
|
t.Fatalf("第二个进程实例 = %q, want instance-2", secondID)
|
|
}
|
|
if firstDir == secondDir {
|
|
t.Fatalf("两个进程的数据目录相同: %s", firstDir)
|
|
}
|
|
if !strings.Contains(secondDir, filepath.Join("instances", "instance-2")) {
|
|
t.Fatalf("第二个进程数据目录 = %q", secondDir)
|
|
}
|
|
}
|
|
|
|
// runInstanceHelper 是子进程分支:占位后打印实例信息,短暂保持占用再释放。
|
|
func runInstanceHelper() {
|
|
lease, err := Acquire()
|
|
if err != nil {
|
|
fmt.Println("ERR:" + err.Error())
|
|
return
|
|
}
|
|
fmt.Printf("%s|%s|%s\n", lease.Name(), lease.ID(), lease.Paths().DataDir)
|
|
time.Sleep(2 * time.Second)
|
|
lease.Release()
|
|
}
|
|
|
|
type instanceHelper struct {
|
|
cmd *exec.Cmd
|
|
lines *bufio.Scanner
|
|
}
|
|
|
|
func startInstanceHelper(t *testing.T, baseDir string) *instanceHelper {
|
|
t.Helper()
|
|
cmd := exec.Command(os.Args[0], "-test.run=TestAcquireIsExclusiveAcrossProcesses")
|
|
cmd.Env = append(os.Environ(),
|
|
"HUYA_INSTANCE_HELPER=1",
|
|
"HUYA_DATA_DIR="+baseDir,
|
|
"HUYA_LOG_DIR=",
|
|
)
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return &instanceHelper{cmd: cmd, lines: bufio.NewScanner(stdout)}
|
|
}
|
|
|
|
// read 读取子进程输出的 "名称|标识|数据目录"。
|
|
func (h *instanceHelper) read(t *testing.T) (name, id, dir string) {
|
|
t.Helper()
|
|
if !h.lines.Scan() {
|
|
t.Fatalf("子进程未输出实例信息: %v", h.lines.Err())
|
|
}
|
|
parts := strings.SplitN(strings.TrimSpace(h.lines.Text()), "|", 3)
|
|
if len(parts) != 3 {
|
|
t.Fatalf("子进程输出异常: %q", h.lines.Text())
|
|
}
|
|
return parts[0], parts[1], parts[2]
|
|
}
|
|
|
|
func (h *instanceHelper) stop() {
|
|
if h.cmd.Process != nil {
|
|
_ = h.cmd.Wait()
|
|
}
|
|
}
|