Files
2026-11-02 23:37:45 -05:00

351 lines
13 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package wine
import (
"bytes"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
)
func write_helper_file(t *testing.T, path, data string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(data), 0700); err != nil {
t.Fatal(err)
}
}
func TestProtonLoaderBypassesProxy(t *testing.T) {
root := t.TempDir()
loader := filepath.Join(root, "Proton", "files", "bin", "wine")
preloader := filepath.Join(root, "Proton", "files", "lib", "wine", "i386-unix", "wine64-preloader")
proxy := filepath.Join(root, "proxy", "wine")
write_helper_file(t, loader, "loader")
write_helper_file(t, preloader, "preloader")
write_helper_file(t, proxy, "#!/bin/sh\n# PROTONTRICKS_STEAM_RUNTIME\n")
for _, configured := range []string{"", proxy, loader} {
if got := wine_loader(preloader, configured); got != loader {
t.Fatalf("loader = %q, want %q", got, loader)
}
}
}
func TestContainerHelperPublishesImmutableCompleteSets(t *testing.T) {
root := t.TempDir()
t.Setenv("XDG_STATE_HOME", filepath.Join(root, "state"))
helper := filepath.Join(root, "installed", "wine-access.exe")
names := []string{"wine-access.exe", "wine-access.exe.so", "wine-access-menu.dll", "wine-access-menu32.dll", "wine-access-menu-host32.exe"}
for _, name := range names {
write_helper_file(t, filepath.Join(filepath.Dir(helper), name), name)
}
environment := map[string]string{"PRESSURE_VESSEL_RUNTIME": "runtime"}
original, err := stage_helper(helper, environment)
if err != nil {
t.Fatal(err)
}
if original == helper {
t.Fatal("helper was not staged")
}
for _, name := range names {
data, err := os.ReadFile(filepath.Join(filepath.Dir(original), name))
if err != nil || string(data) != name {
t.Fatalf("incomplete set: %s: %v", name, err)
}
}
write_helper_file(t, helper+".so", "new sidecar")
updated, err := stage_helper(helper, environment)
if err != nil || updated == original {
t.Fatalf("updated set = %q, %v", updated, err)
}
data, _ := os.ReadFile(original + ".so")
if string(data) != "wine-access.exe.so" {
t.Fatal("old sidecar overwritten")
}
write_helper_file(t, updated+".so", "tampered")
if _, err := stage_helper(helper, environment); err == nil {
t.Fatal("accepted modified staging")
}
if err := os.Remove(helper + ".so"); err != nil {
t.Fatal(err)
}
if _, err := stage_helper(helper, environment); err == nil {
t.Fatal("accepted incomplete source set")
}
if got, err := stage_helper(helper, nil); err != nil || got != helper {
t.Fatalf("ordinary Wine = %q, %v", got, err)
}
}
func TestListedProgramDoesNotForceWholesaleMode(t *testing.T) {
t.Setenv("WINE_ACCESS_NO_DIALOG_READER", "")
// A listed program is gated natively by the helper. Its presence must
// never add the prefix-wide --no-dialog-reader, or ordinary programs in
// the same prefix lose dialog reading.
listed := Settings{ExcludedPrograms: []string{"Game.EXE", "game.exe"}}
if wholesale_controller_only(listed) {
t.Fatal("listed programs selected wholesale mode")
}
arguments := helper_arguments(wholesale_controller_only(listed), listed)
if len(arguments) != 2 || arguments[0] != "--exclude-hook-program" || arguments[1] != "game.exe" {
t.Fatalf("mixed-prefix arguments = %v", arguments)
}
global := Settings{ControllerOnly: true, ExcludedPrograms: []string{"Game.exe"}}
if !wholesale_controller_only(global) {
t.Fatal("global controller-only switch ignored")
}
arguments = helper_arguments(true, global)
if arguments[len(arguments)-1] != "--no-dialog-reader" {
t.Fatalf("global arguments = %v", arguments)
}
t.Setenv("WINE_ACCESS_NO_DIALOG_READER", "1")
if !wholesale_controller_only(Settings{}) {
t.Fatal("environment override ignored")
}
}
func TestHelperArgumentsValidateProgramNames(t *testing.T) {
settings := Settings{ExcludedPrograms: []string{
"Game.exe", "Another Game.exe", "Café.exe", "game.EXE",
"Game\t.exe", "Game\x01.exe", "Game\x1f.exe", `C:\Game.exe`,
strings.Repeat("é", 126) + ".exe",
}}
want := []string{
"--exclude-hook-program", "another game.exe",
"--exclude-hook-program", "café.exe",
"--exclude-hook-program", "game.exe",
}
if got := helper_arguments(false, settings); !reflect.DeepEqual(got, want) {
t.Fatalf("validated helper arguments = %q, want %q", got, want)
}
}
func TestProtonInfrastructureDoesNotKeepHelperAlive(t *testing.T) {
for _, program := range []string{`C:\windows\system32\steam.exe`, `C:\windows\syswow64\steam.exe`, "xalia.exe", "conhost.exe"} {
if applicationProcess(program) {
t.Fatalf("infrastructure counted as app: %s", program)
}
}
for _, program := range []string{`C:\Program Files\Steam\steam.exe`, "Game.exe", "explorer.exe"} {
if !applicationProcess(program) {
t.Fatalf("app ignored: %s", program)
}
}
}
func TestLaunchForwardsProtonSyncAndKeepsReaderBus(t *testing.T) {
root := t.TempDir()
output := filepath.Join(root, "environment")
helper := filepath.Join(root, "helper")
write_helper_file(t, helper, "#!/bin/sh\nenv > \"$TEST_WINE_ENV\"\n")
t.Setenv("TEST_WINE_ENV", output)
t.Setenv("DBUS_SESSION_BUS_ADDRESS", "unix:path=/reader/bus")
manager := New(nil)
manager.helper = helper
child := &process{}
source := prefix{path: root, environment: map[string]string{"WINEESYNC": "1", "WINEFSYNC": "1", "XAUTHORITY": "/fixture/auth", "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/pressure-vessel/bus"}}
if err := manager.launch(child, source, time.Now(), Settings{}); err != nil {
t.Fatal(err)
}
select {
case <-child.done:
case <-time.After(3 * time.Second):
stop(child)
t.Fatal("helper did not exit")
}
data, err := os.ReadFile(output)
if err != nil {
t.Fatal(err)
}
for _, value := range []string{"WINEESYNC=1\n", "WINEFSYNC=1\n", "XAUTHORITY=/fixture/auth\n", "DBUS_SESSION_BUS_ADDRESS=unix:path=/reader/bus\n"} {
if !bytes.Contains(data, []byte(value)) {
t.Fatalf("missing %q", value)
}
}
}
func TestDiscoverMixedPrefixKeepsOrdinaryProgramsReadable(t *testing.T) {
t.Setenv("WINE_ACCESS_NO_DIALOG_READER", "")
root := t.TempDir()
procRoot := filepath.Join(root, "proc")
loader := filepath.Join(root, "wine")
write_helper_file(t, loader, "loader")
makeProcess := func(pid, program string) {
t.Helper()
base := filepath.Join(procRoot, pid)
write_helper_file(t, filepath.Join(base, "cmdline"), program+"\x00")
write_helper_file(t, filepath.Join(base, "environ"), "WINEPREFIX="+root+"\x00")
if err := os.Symlink(loader, filepath.Join(base, "exe")); err != nil {
t.Fatal(err)
}
}
makeProcess("101", "launcher.exe")
helper := filepath.Join(root, "helper")
write_helper_file(t, helper, "#!/bin/sh\nexec sleep 30\n")
current := Settings{ExcludedPrograms: []string{"GAME.exe"}}
manager := New(nil)
manager.helper, manager.procRoot = helper, procRoot
manager.SettingsSource = func() Settings { return current }
active := map[string]*process{}
defer func() {
for _, child := range active {
stop(child)
}
}()
// Launcher only: the exclusion flag is already armed, in normal mode.
manager.poll(active)
child := active[root]
if child == nil || child.cmd == nil || child.controllerOnly {
t.Fatal("launcher helper did not start normally")
}
if !child.exclusions["game.exe"] {
t.Fatal("launcher helper lacks the armed exclusion flag")
}
originalPID := child.cmd.Process.Pid
// The listed game and an ordinary program appear. The helper already
// carries the flag, so nothing restarts and nothing goes wholesale:
// the ordinary program keeps dialog reading by construction, since
// --no-dialog-reader was never passed.
makeProcess("102", "Game.exe")
makeProcess("103", "notepad.exe")
discovered := discover(procRoot, uint32(os.Getuid()))
if len(discovered[root].programs) != 3 {
t.Fatalf("programs = %v", discovered[root].programs)
}
manager.poll(active)
if child.controllerOnly || child.cmd == nil || child.cmd.Process.Pid != originalPID {
t.Fatal("listed game restarted or flipped the helper wholesale")
}
// The listed game disappears: still no restart and no mode change.
if err := os.RemoveAll(filepath.Join(procRoot, "102")); err != nil {
t.Fatal(err)
}
manager.poll(active)
if child.controllerOnly || child.cmd == nil || child.cmd.Process.Pid != originalPID {
t.Fatal("helper restarted when the listed game disappeared")
}
// A listing for a program that is not running waits for the next
// natural spawn: no restart for prospective protection.
current.ExcludedPrograms = []string{"GAME.exe", "Absent.exe"}
manager.poll(active)
if child.cmd.Process.Pid != originalPID {
t.Fatal("helper restarted for a listed program that is not running")
}
// A newly listed program that is currently discovered but lacks its
// flag restarts the helper once, in the same mode, carrying the flag.
current.ExcludedPrograms = []string{"GAME.exe", "Notepad.exe"}
manager.poll(active)
if child.controllerOnly || child.cmd == nil || child.cmd.Process.Pid == originalPID {
t.Fatal("newly listed running program did not restart the helper in place")
}
if !child.exclusions["notepad.exe"] {
t.Fatal("restarted helper lacks the new exclusion flag")
}
pid := child.cmd.Process.Pid
manager.poll(active)
if child.cmd.Process.Pid != pid {
t.Fatal("helper restarted more than once for one listing")
}
// Removing a listing never downgrades or restarts a running helper.
current.ExcludedPrograms = []string{"GAME.exe"}
manager.poll(active)
if child.cmd.Process.Pid != pid {
t.Fatal("helper restarted when a listing was removed")
}
// The global switch still upgrades a running helper to wholesale, and
// clearing it never downgrades the running helper.
current.ControllerOnly = true
manager.poll(active)
if !child.controllerOnly || child.cmd == nil || child.cmd.Process.Pid == pid {
t.Fatal("global switch did not upgrade the helper to wholesale")
}
pid = child.cmd.Process.Pid
current.ControllerOnly = false
manager.poll(active)
if !child.controllerOnly || child.cmd.Process.Pid != pid {
t.Fatal("wholesale helper downgraded after the global switch cleared")
}
}
func TestUnconfirmedHelperRestartsWholesaleOnceAndIsRemembered(t *testing.T) {
t.Setenv("WINE_ACCESS_NO_DIALOG_READER", "")
root := t.TempDir()
procRoot := filepath.Join(root, "proc")
loader := filepath.Join(root, "wine")
write_helper_file(t, loader, "loader")
base := filepath.Join(procRoot, "101")
write_helper_file(t, filepath.Join(base, "cmdline"), "launcher.exe\x00")
write_helper_file(t, filepath.Join(base, "environ"), "WINEPREFIX="+root+"\x00")
if err := os.Symlink(loader, filepath.Join(base, "exe")); err != nil {
t.Fatal(err)
}
helper := filepath.Join(root, "helper")
// An old helper that ignores the exclusion flags reports zero.
script := "#!/bin/sh\n" +
"echo 'wine-access: ready {\"reader\":\"org.example.Reader\",\"controls_enabled\":true,\"hook_exclusions\":0}' >&2\n" +
"exec sleep 30\n"
write_helper_file(t, helper, script)
manager := New(nil)
manager.helper, manager.procRoot = helper, procRoot
manager.SettingsSource = func() Settings {
return Settings{ExcludedPrograms: []string{"Game.exe"}}
}
active := map[string]*process{}
defer func() {
for _, child := range active {
stop(child)
}
}()
manager.poll(active)
child := active[root]
if child == nil || child.cmd == nil || child.controllerOnly {
t.Fatal("first helper did not start normally")
}
if got := strings.Join(child.cmd.Args[1:], " "); got != "--exclude-hook-program game.exe" {
t.Fatalf("first launch arguments = %q", got)
}
firstPID := child.cmd.Process.Pid
deadline := time.Now().Add(2 * time.Second)
for !child.readySeen.Load() && time.Now().Before(deadline) {
time.Sleep(10 * time.Millisecond)
}
if !child.readySeen.Load() {
t.Fatal("manager did not consume the ready record")
}
manager.poll(active)
if !child.controllerOnly || child.cmd == nil || child.cmd.Process.Pid == firstPID {
t.Fatal("unconfirmed helper did not restart wholesale")
}
if got := strings.Join(child.cmd.Args[1:], " "); got != "--exclude-hook-program game.exe --no-dialog-reader" {
t.Fatalf("wholesale launch arguments = %q", got)
}
wholesalePID := child.cmd.Process.Pid
manager.poll(active)
if child.cmd.Process.Pid != wholesalePID {
t.Fatal("unconfirmed helper restarted wholesale more than once")
}
// The binary is remembered: a later spawn goes wholesale directly.
stop(child)
fresh := map[string]*process{}
defer func() {
for _, child := range fresh {
stop(child)
}
}()
manager.poll(fresh)
replacement := fresh[root]
if replacement == nil || replacement.cmd == nil || !replacement.controllerOnly {
t.Fatal("remembered helper did not start wholesale directly")
}
if got := strings.Join(replacement.cmd.Args[1:], " "); got != "--exclude-hook-program game.exe --no-dialog-reader" {
t.Fatalf("remembered launch arguments = %q", got)
}
}