355 lines
12 KiB
Go
355 lines
12 KiB
Go
package wine
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDiscoverWinePrefixesAndExcludeManagedHelper(t *testing.T) {
|
|
root := t.TempDir()
|
|
loader := filepath.Join(root, "wine-preloader")
|
|
if err := os.WriteFile(loader, []byte("#!/bin/sh\n"), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
prefixPath := filepath.Join(root, "prefix")
|
|
if err := os.Mkdir(prefixPath, 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
procRoot := filepath.Join(root, "proc")
|
|
if err := os.Mkdir(procRoot, 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
makeProcess := func(pid int, exe, environment string) {
|
|
t.Helper()
|
|
path := filepath.Join(procRoot, strconv.Itoa(pid))
|
|
if err := os.Mkdir(path, 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(exe, filepath.Join(path, "exe")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(path, "environ"), []byte(environment), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(path, "cmdline"), []byte("C:\\app\\application.exe\x00"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
makeProcess(101, loader, "WINEPREFIX="+prefixPath+"\x00DISPLAY=:7\x00")
|
|
makeProcess(102, loader, "WINEPREFIX="+prefixPath+"\x00"+managedKey+"=1\x00")
|
|
makeProcess(103, "/bin/sleep", "WINEPREFIX="+prefixPath+"\x00")
|
|
makeProcess(104, loader, "WINEPREFIX="+filepath.Join(root, "idle-prefix")+"\x00")
|
|
if err := os.WriteFile(filepath.Join(procRoot, "104", "cmdline"), []byte("C:\\windows\\system32\\winedevice.exe\x00"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := discover(procRoot, uint32(os.Getuid()))
|
|
if len(got) != 1 {
|
|
t.Fatalf("prefixes = %+v, want one unmanaged Wine prefix", got)
|
|
}
|
|
selected := got[prefixPath]
|
|
if selected.path != prefixPath || selected.environment["DISPLAY"] != ":7" {
|
|
t.Fatalf("prefix = %+v", selected)
|
|
}
|
|
if selected.loader != "" {
|
|
t.Fatalf("missing derived wine loader should fall back to launcher default, got %q", selected.loader)
|
|
}
|
|
}
|
|
|
|
func TestDiscoverDefaultWinePrefix(t *testing.T) {
|
|
t.Setenv("HOME", t.TempDir())
|
|
for _, test := range []struct {
|
|
name, extra, want string
|
|
}{
|
|
{name: "unset prefix", want: "home/.wine"},
|
|
{name: "empty prefix", extra: "WINEPREFIX=\x00", want: "home/.wine"},
|
|
{name: "explicit prefix", extra: "WINEPREFIX={root}/custom\x00STEAM_COMPAT_DATA_PATH={root}/steam\x00", want: "custom"},
|
|
{name: "Steam prefix", extra: "STEAM_COMPAT_DATA_PATH={root}/steam\x00", want: "steam/pfx"},
|
|
{name: "managed helper", extra: managedKey + "=1\x00"},
|
|
{name: "missing home", extra: "HOME=\x00"},
|
|
{name: "relative home", extra: "HOME=relative\x00"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
root := t.TempDir()
|
|
loader := filepath.Join(root, "wine")
|
|
if err := os.WriteFile(loader, []byte("#!/bin/sh\n"), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
proc := filepath.Join(root, "200")
|
|
if err := os.Mkdir(proc, 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(loader, filepath.Join(proc, "exe")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(proc, "cmdline"), []byte("application.exe\x00"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
environment := "HOME=" + filepath.Join(root, "home") + "\x00DISPLAY=:7\x00" +
|
|
strings.ReplaceAll(test.extra, "{root}", root)
|
|
if err := os.WriteFile(filepath.Join(proc, "environ"), []byte(environment), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := discover(root, uint32(os.Getuid()))
|
|
if test.want == "" {
|
|
if len(got) != 0 {
|
|
t.Fatalf("prefixes = %+v, want none", got)
|
|
}
|
|
return
|
|
}
|
|
want := filepath.Join(root, test.want)
|
|
if len(got) != 1 {
|
|
t.Fatalf("prefixes = %+v, want only %q", got, want)
|
|
}
|
|
selected := got[want]
|
|
if selected.path != want || selected.loader != loader || selected.environment["DISPLAY"] != ":7" {
|
|
t.Fatalf("prefix = %+v, want %q with its loader and display", selected, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHelperPathPrefersConfiguredThenLocalHelper(t *testing.T) {
|
|
root := t.TempDir()
|
|
t.Setenv("HOME", root)
|
|
t.Setenv("CTHULHU_WINE_ACCESS_HELPER", "")
|
|
|
|
legacy := filepath.Join(root, ".local", "libexec", "cthulhu", "wine", "cthulhu-wine-access.exe")
|
|
if err := os.MkdirAll(filepath.Dir(legacy), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(legacy, []byte("#!/bin/sh\n"), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := helperPath(); got != legacy {
|
|
t.Fatalf("helperPath() = %q, want existing user helper %q", got, legacy)
|
|
}
|
|
|
|
local := filepath.Join(root, ".local", "libexec", "wine-access", "wine-access.exe")
|
|
if err := os.MkdirAll(filepath.Dir(local), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(local, []byte("#!/bin/sh\n"), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := helperPath(); got != local {
|
|
t.Fatalf("helperPath() = %q, want %q", got, local)
|
|
}
|
|
|
|
configured := filepath.Join(root, "configured-helper")
|
|
if err := os.WriteFile(configured, []byte("#!/bin/sh\n"), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("CTHULHU_WINE_ACCESS_HELPER", configured)
|
|
if got := helperPath(); got != configured {
|
|
t.Fatalf("helperPath() = %q, want %q", got, configured)
|
|
}
|
|
}
|
|
|
|
func TestFindHelperPrecedenceAndExecutableFiltering(t *testing.T) {
|
|
root := t.TempDir()
|
|
t.Setenv("CTHULHU_WINE_ACCESS_HELPER", "")
|
|
t.Setenv("PATH", root)
|
|
candidates := []string{
|
|
filepath.Join(root, "user", "wine-access.exe"),
|
|
filepath.Join(root, "usr-local", "wine-access.exe"),
|
|
filepath.Join(root, "usr", "wine-access.exe"),
|
|
}
|
|
for _, path := range candidates {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if got := findHelper(candidates); got != "" {
|
|
t.Fatalf("missing helpers = %q, want empty", got)
|
|
}
|
|
legacy := filepath.Join(root, "cthulhu-wine-access.exe")
|
|
pathHelper := filepath.Join(root, "wine-access.exe")
|
|
for _, path := range []string{legacy, pathHelper, candidates[2], candidates[1], candidates[0]} {
|
|
if err := os.WriteFile(path, []byte("#!/bin/sh\n"), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := findHelper(candidates); got != path {
|
|
t.Fatalf("findHelper() = %q, want %q", got, path)
|
|
}
|
|
}
|
|
if err := os.Chmod(candidates[0], 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("CTHULHU_WINE_ACCESS_HELPER", candidates[0])
|
|
if got := findHelper(candidates); got != candidates[1] {
|
|
t.Fatalf("non-executable override/user helper = %q, want %q", got, candidates[1])
|
|
}
|
|
t.Setenv("CTHULHU_WINE_ACCESS_HELPER", root)
|
|
if got := findHelper(candidates); got != candidates[1] {
|
|
t.Fatalf("directory override = %q, want %q", got, candidates[1])
|
|
}
|
|
t.Setenv("CTHULHU_WINE_ACCESS_HELPER", pathHelper)
|
|
if got := findHelper(candidates); got != pathHelper {
|
|
t.Fatalf("executable override = %q, want %q", got, pathHelper)
|
|
}
|
|
}
|
|
|
|
func TestManagerLaunchesAndStopsHelperForActivePrefix(t *testing.T) {
|
|
root := t.TempDir()
|
|
procRoot := filepath.Join(root, "proc")
|
|
if err := os.Mkdir(procRoot, 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loader := filepath.Join(root, "wine")
|
|
if err := os.WriteFile(loader, []byte("#!/bin/sh\n"), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
prefixPath := filepath.Join(root, "prefix")
|
|
if err := os.Mkdir(prefixPath, 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
proc := filepath.Join(procRoot, "300")
|
|
if err := os.Mkdir(proc, 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(loader, filepath.Join(proc, "exe")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(proc, "cmdline"), []byte("application.exe\x00"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(proc, "environ"), []byte("WINEPREFIX="+prefixPath+"\x00WINELOADER="+loader+"\x00"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
output := filepath.Join(root, "helper-output")
|
|
t.Setenv("CTHULHU_TEST_WINE_OUTPUT", output)
|
|
helper := filepath.Join(root, "helper")
|
|
script := "#!/bin/sh\nprintf '%s\\n' \"$WINEPREFIX\" \"$CTHULHU_WINE_ACCESS_MANAGED\" > \"$CTHULHU_TEST_WINE_OUTPUT\"\nexec sleep 30\n"
|
|
if err := os.WriteFile(helper, []byte(script), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manager := &Manager{logger: slog.New(slog.DiscardHandler), helper: helper, procRoot: procRoot}
|
|
if err := manager.Start(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer manager.Stop(context.Background())
|
|
deadline := time.Now().Add(time.Second)
|
|
var contents []byte
|
|
for time.Now().Before(deadline) {
|
|
contents, _ = os.ReadFile(output)
|
|
if len(contents) != 0 {
|
|
break
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
if string(contents) != prefixPath+"\n1\n" {
|
|
t.Fatalf("helper environment = %q", contents)
|
|
}
|
|
stopCtx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
if err := manager.Stop(stopCtx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestDiscoverSteamCompatPrefix(t *testing.T) {
|
|
root := t.TempDir()
|
|
loader := filepath.Join(root, "wine")
|
|
if err := os.WriteFile(loader, []byte("#!/bin/sh\n"), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
proc := filepath.Join(root, "200")
|
|
if err := os.Mkdir(proc, 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(loader, filepath.Join(proc, "exe")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(proc, "cmdline"), []byte("application.exe\x00"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(proc, "environ"), []byte(strings.Join([]string{
|
|
"STEAM_COMPAT_DATA_PATH=" + root, "WINELOADER=" + loader, ""}, "\x00")), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := discover(root, uint32(os.Getuid()))
|
|
selected := got[filepath.Join(root, "pfx")]
|
|
if selected.loader != loader {
|
|
t.Fatalf("Steam loader = %q, want %q", selected.loader, loader)
|
|
}
|
|
}
|
|
|
|
func TestApplicationProcessExcludesWineServices(t *testing.T) {
|
|
for _, program := range []string{"services.exe", "winedevice.exe", "svchost.exe", "plugplay.exe", "rpcss.exe", "lsass.exe", "wineboot.exe", "winemenubuilder.exe"} {
|
|
if applicationProcess("C:\\windows\\system32\\" + strings.ToUpper(program) + "\x00") {
|
|
t.Errorf("Wine service %q counted as an application", program)
|
|
}
|
|
}
|
|
for _, test := range []struct {
|
|
commandLine string
|
|
want bool
|
|
}{
|
|
{"", false},
|
|
{"C:\\windows\\explorer.exe\x00/desktop\x00", false},
|
|
{"C:\\windows\\explorer.exe\x00/DESKTOP=Default,1024x768\x00", false},
|
|
{"C:\\windows\\explorer.exe\x00C:\\documents\x00", true},
|
|
{"C:\\Program Files\\Application\\app.exe\x00/desktop\x00", true},
|
|
{"C:\\windows\\system32\\notepad.exe\x00", true},
|
|
{"/usr/bin/wine\x00application.exe\x00", true},
|
|
} {
|
|
if got := applicationProcess(test.commandLine); got != test.want {
|
|
t.Errorf("applicationProcess(%q) = %v, want %v", test.commandLine, got, test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestManagerStopsHelperWhenOnlyWineServicesRemain(t *testing.T) {
|
|
root := t.TempDir()
|
|
proc := filepath.Join(root, "101")
|
|
if err := os.Mkdir(proc, 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loader := filepath.Join(root, "wine")
|
|
helper := filepath.Join(root, "helper")
|
|
for _, path := range []string{loader, helper} {
|
|
if err := os.WriteFile(path, []byte("#!/bin/sh\nexec sleep 30\n"), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := os.Symlink(loader, filepath.Join(proc, "exe")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
prefixPath := filepath.Join(root, "prefix")
|
|
if err := os.WriteFile(filepath.Join(proc, "environ"), []byte("WINEPREFIX="+prefixPath+"\x00"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(proc, "cmdline"), []byte("application.exe\x00"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manager := &Manager{logger: slog.New(slog.DiscardHandler), helper: helper, procRoot: root}
|
|
active := make(map[string]*process)
|
|
manager.poll(active)
|
|
child := active[prefixPath]
|
|
if child == nil || child.cmd == nil {
|
|
t.Fatal("application did not start a helper")
|
|
}
|
|
defer stop(child)
|
|
if err := os.WriteFile(filepath.Join(proc, "cmdline"), []byte("C:\\windows\\system32\\winedevice.exe\x00"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manager.poll(active)
|
|
if child.cmd == nil {
|
|
t.Fatal("helper stopped before the idle grace period")
|
|
}
|
|
child.seen = time.Now().Add(-11 * time.Second)
|
|
manager.poll(active)
|
|
if len(active) != 0 || child.cmd != nil {
|
|
t.Fatal("Wine service kept the helper alive after the idle grace period")
|
|
}
|
|
manager.poll(active)
|
|
if len(active) != 0 {
|
|
t.Fatal("Wine service restarted the helper")
|
|
}
|
|
}
|