Add Wine ready-record and mixed-prefix spawn semantics

Phase 1 of the Wine/Proton stage (decisions 2, 5, 16, 17, 18, 24 in docs/testing/wine-proton.md): classify the helper exclusion-count line as info, parse the ready hook_exclusions count, mirror programs=[...] extra=[...] in the helper-start record, and stop passing prefix-wide --no-dialog-reader merely because a listed program is present, so ordinary programs in a mixed prefix keep dialog reading. Wholesale mode remains for the global controller-only switch, the environment override, and helpers whose ready record does not confirm the exclusions sent; unconfirmed binaries are remembered. A newly listed running program restarts its helper once in the same mode; removals never downgrade.
This commit is contained in:
hjozwiak committed 2026-11-01 23:27:55 -05:00
1 parent 6ed0e36960
commit 71093286c7
5 files changed
+459 -55

No files matched your search

+147 -17
View File
@@ -7,6 +7,7 @@ import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
@@ -82,24 +83,29 @@ func TestContainerHelperPublishesImmutableCompleteSets(t *testing.T) {
}
}
func TestControllerOnlyExclusionsReachLauncherBeforeGameStarts(t *testing.T) {
func TestListedProgramDoesNotForceWholesaleMode(t *testing.T) {
t.Setenv("WINE_ACCESS_NO_DIALOG_READER", "")
manager := &Manager{ExcludedPrograms: []string{"Game.EXE"}}
launcher := prefix{programs: map[string]bool{"launcher.exe": true}}
arguments := manager.helper_arguments(manager.controller_only(launcher))
// 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.
manager := &Manager{ExcludedPrograms: []string{"Game.EXE", "game.exe"}}
if manager.wholesale_controller_only() {
t.Fatal("listed programs selected wholesale mode")
}
arguments := manager.helper_arguments(manager.wholesale_controller_only())
if len(arguments) != 2 || arguments[0] != "--exclude-hook-program" || arguments[1] != "game.exe" {
t.Fatalf("launcher arguments = %v", arguments)
t.Fatalf("mixed-prefix arguments = %v", arguments)
}
launcher.programs["game.exe"] = true
if !manager.controller_only(launcher) {
t.Fatal("game did not select controller-only")
global := &Manager{ControllerOnly: true, ExcludedPrograms: []string{"Game.exe"}}
if !global.wholesale_controller_only() {
t.Fatal("global controller-only switch ignored")
}
arguments = manager.helper_arguments(true)
arguments = global.helper_arguments(true)
if arguments[len(arguments)-1] != "--no-dialog-reader" {
t.Fatalf("game arguments = %v", arguments)
t.Fatalf("global arguments = %v", arguments)
}
t.Setenv("WINE_ACCESS_NO_DIALOG_READER", "1")
if !(&Manager{}).controller_only(prefix{}) {
if !(&Manager{}).wholesale_controller_only() {
t.Fatal("environment override ignored")
}
}
@@ -148,7 +154,8 @@ func TestLaunchForwardsProtonSyncAndKeepsReaderBus(t *testing.T) {
}
}
func TestDiscoverAggregatesGamesAndEscalatesHelper(t *testing.T) {
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")
@@ -174,27 +181,150 @@ func TestDiscoverAggregatesGamesAndEscalatesHelper(t *testing.T) {
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) != 2 {
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("helper did not switch to controller-only")
if child.controllerOnly || child.cmd == nil || child.cmd.Process.Pid != originalPID {
t.Fatal("listed game restarted or flipped the helper wholesale")
}
pid := child.cmd.Process.Pid
// 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.
manager.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.
manager.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.
manager.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.
manager.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
manager.ControllerOnly = false
manager.poll(active)
if !child.controllerOnly || child.cmd.Process.Pid != pid {
t.Fatal("controller-only helper downgraded after game disappeared")
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.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)
}
}