Launch WINEDEBUG defaults to err+all,warn+waylanddrv; BNETCTL_WINEDEBUG overrides it. Wine launch stderr goes to $XDG_STATE_HOME/bnetctl/logs with latest.log; keeps 3 logs, max 100 MiB of old logs. Reviewed (go-reviewer); go test -race, vet, staticcheck, golangci-lint pass.
#7 feat(wine): keep Wayland driver warnings in a rotated log
M README.md +16 -1
| @@ -69,6 +69,12 @@ bnetctl clean -a # Also remove desktop entries and data | |||
| 69 | 69 | ||
| 70 | 70 | For example: `BNETCTL_WINE=/usr/bin/wine bnetctl launch`. | |
| 71 | 71 | ||
| 72 | + | `BNETCTL_WINEDEBUG`, when set, overrides the launch `WINEDEBUG` verbatim | |
| 73 | + | (including an empty value). The default is `err+all,warn+waylanddrv`, retaining | |
| 74 | + | errors and Wayland driver warnings at low cost. To silence Wine diagnostics, | |
| 75 | + | use `BNETCTL_WINEDEBUG=-all bnetctl launch`. Helper commands such as wineboot, | |
| 76 | + | registry edits, and taskkill keep diagnostics disabled. | |
| 77 | + | ||
| 72 | 78 | ### HDR | |
| 73 | 79 | ||
| 74 | 80 | Native Wayland automatically enables HDR for supported World of Warcraft | |
| @@ -112,7 +118,15 @@ X11 mode leaves the user's HDR environment untouched. | |||
| 112 | 118 | Stop/clean wait up to 10 seconds for the lock; retry after maintenance finishes. | |
| 113 | 119 | Launch releases its lock once Wine starts, not at the end of the game session. | |
| 114 | 120 | - **More detail:** run `bnetctl --verbose launch` to see runtime selection and | |
| 115 | - | maintenance diagnostics. Logs are saved to `~/.cache/bnetctl/bnetctl.log`. | |
| 121 | + | maintenance diagnostics. bnetctl logs are saved to `~/.cache/bnetctl/bnetctl.log`. | |
| 122 | + | - **Wine diagnostics:** launch stderr is saved to | |
| 123 | + | `$XDG_STATE_HOME/bnetctl/logs/wine-<timestamp>-<unique>.log` (falling back to | |
| 124 | + | `~/.local/state/bnetctl/logs`), with `latest.log` pointing to the newest log. | |
| 125 | + | Up to 3 launches are retained (~100 MiB of old logs); at launch, older logs | |
| 126 | + | are pruned and old logs over 50 MiB are deleted. The running log is uncapped. | |
| 127 | + | The path is reported once at info level; Wine stdout and bnetctl's own | |
| 128 | + | terminal logs are unchanged. If log setup fails, bnetctl warns and leaves | |
| 129 | + | Wine stderr on the terminal. | |
| 116 | 130 | ||
| 117 | 131 | ## Directories | |
| 118 | 132 | ||
| @@ -126,6 +140,7 @@ are not currently honored. Full cleanup retains the data directory and its | |||
| 126 | 140 | | Prefix | `~/.local/share/bnetctl/prefix` | | |
| 127 | 141 | | Cache | `~/.cache/bnetctl` | | |
| 128 | 142 | | Log | `~/.cache/bnetctl/bnetctl.log` | | |
| 143 | + | | Wine logs | `$XDG_STATE_HOME/bnetctl/logs` (default `~/.local/state/bnetctl/logs`) | | |
| 129 | 144 | ||
| 130 | 145 | ## License | |
| 131 | 146 | ||
| @@ -69,6 +69,12 @@ bnetctl clean -a # Also remove desktop entries and data | |||||
| 69 | 69 | ||||
| 70 | For example: `BNETCTL_WINE=/usr/bin/wine bnetctl launch`. | 70 | For example: `BNETCTL_WINE=/usr/bin/wine bnetctl launch`. | ||
| 71 | 71 | ||||
| 72 | + | `BNETCTL_WINEDEBUG`, when set, overrides the launch `WINEDEBUG` verbatim | |||
| 73 | + | (including an empty value). The default is `err+all,warn+waylanddrv`, retaining | |||
| 74 | + | errors and Wayland driver warnings at low cost. To silence Wine diagnostics, | |||
| 75 | + | use `BNETCTL_WINEDEBUG=-all bnetctl launch`. Helper commands such as wineboot, | |||
| 76 | + | registry edits, and taskkill keep diagnostics disabled. | |||
| 77 | + | ||||
| 72 | ### HDR | 78 | ### HDR | ||
| 73 | 79 | ||||
| 74 | Native Wayland automatically enables HDR for supported World of Warcraft | 80 | Native Wayland automatically enables HDR for supported World of Warcraft | ||
| @@ -112,7 +118,15 @@ X11 mode leaves the user's HDR environment untouched. | |||||
| 112 | Stop/clean wait up to 10 seconds for the lock; retry after maintenance finishes. | 118 | Stop/clean wait up to 10 seconds for the lock; retry after maintenance finishes. | ||
| 113 | Launch releases its lock once Wine starts, not at the end of the game session. | 119 | Launch releases its lock once Wine starts, not at the end of the game session. | ||
| 114 | - **More detail:** run `bnetctl --verbose launch` to see runtime selection and | 120 | - **More detail:** run `bnetctl --verbose launch` to see runtime selection and | ||
| 115 | - | maintenance diagnostics. Logs are saved to `~/.cache/bnetctl/bnetctl.log`. | 121 | + | maintenance diagnostics. bnetctl logs are saved to `~/.cache/bnetctl/bnetctl.log`. |
| 122 | + | - **Wine diagnostics:** launch stderr is saved to | |||
| 123 | + | `$XDG_STATE_HOME/bnetctl/logs/wine-<timestamp>-<unique>.log` (falling back to | |||
| 124 | + | `~/.local/state/bnetctl/logs`), with `latest.log` pointing to the newest log. | |||
| 125 | + | Up to 3 launches are retained (~100 MiB of old logs); at launch, older logs | |||
| 126 | + | are pruned and old logs over 50 MiB are deleted. The running log is uncapped. | |||
| 127 | + | The path is reported once at info level; Wine stdout and bnetctl's own | |||
| 128 | + | terminal logs are unchanged. If log setup fails, bnetctl warns and leaves | |||
| 129 | + | Wine stderr on the terminal. | |||
| 116 | 130 | ||||
| 117 | ## Directories | 131 | ## Directories | ||
| 118 | 132 | ||||
| @@ -126,6 +140,7 @@ are not currently honored. Full cleanup retains the data directory and its | |||||
| 126 | | Prefix | `~/.local/share/bnetctl/prefix` | | 140 | | Prefix | `~/.local/share/bnetctl/prefix` | | ||
| 127 | | Cache | `~/.cache/bnetctl` | | 141 | | Cache | `~/.cache/bnetctl` | | ||
| 128 | | Log | `~/.cache/bnetctl/bnetctl.log` | | 142 | | Log | `~/.cache/bnetctl/bnetctl.log` | | ||
| 143 | + | | Wine logs | `$XDG_STATE_HOME/bnetctl/logs` (default `~/.local/state/bnetctl/logs`) | | |||
| 129 | 144 | ||||
| 130 | ## License | 145 | ## License | ||
| 131 | 146 | ||||
M internal/adapters/env/gpu.go +5 -2
| @@ -146,8 +146,11 @@ func BuildGPUEnvForDisplayHDR(driver DisplayDriver, noHDR bool) *domain.WineEnv | |||
| 146 | 146 | env.Vars["WINEESYNC"] = "1" | |
| 147 | 147 | env.Vars["WINEFSYNC"] = "1" | |
| 148 | 148 | ||
| 149 | - | // Suppress Wine debug output for performance | |
| 150 | - | env.Vars["WINEDEBUG"] = "-all" | |
| 149 | + | // Keep low-cost problem diagnostics unless explicitly overridden. | |
| 150 | + | env.Vars["WINEDEBUG"] = "err+all,warn+waylanddrv" | |
| 151 | + | if debug, set := os.LookupEnv("BNETCTL_WINEDEBUG"); set { | |
| 152 | + | env.Vars["WINEDEBUG"] = debug | |
| 153 | + | } | |
| 151 | 154 | ||
| 152 | 155 | // FreeType hinting for better font rendering in Wine | |
| 153 | 156 | env.Vars["FREETYPE_PROPERTIES"] = "truetype:interpreter-version=35" | |
| @@ -146,8 +146,11 @@ func BuildGPUEnvForDisplayHDR(driver DisplayDriver, noHDR bool) *domain.WineEnv | |||||
| 146 | env.Vars["WINEESYNC"] = "1" | 146 | env.Vars["WINEESYNC"] = "1" | ||
| 147 | env.Vars["WINEFSYNC"] = "1" | 147 | env.Vars["WINEFSYNC"] = "1" | ||
| 148 | 148 | ||||
| 149 | - | // Suppress Wine debug output for performance | 149 | + | // Keep low-cost problem diagnostics unless explicitly overridden. |
| 150 | - | env.Vars["WINEDEBUG"] = "-all" | 150 | + | env.Vars["WINEDEBUG"] = "err+all,warn+waylanddrv" |
| 151 | + | if debug, set := os.LookupEnv("BNETCTL_WINEDEBUG"); set { | |||
| 152 | + | env.Vars["WINEDEBUG"] = debug | |||
| 153 | + | } | |||
| 151 | 154 | ||||
| 152 | // FreeType hinting for better font rendering in Wine | 155 | // FreeType hinting for better font rendering in Wine | ||
| 153 | env.Vars["FREETYPE_PROPERTIES"] = "truetype:interpreter-version=35" | 156 | env.Vars["FREETYPE_PROPERTIES"] = "truetype:interpreter-version=35" | ||
M internal/adapters/env/gpu_test.go +29 -0
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package env | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "os" | |
| 4 | 5 | "slices" | |
| 5 | 6 | "testing" | |
| 6 | 7 | ) | |
| @@ -55,3 +56,31 @@ func TestBuildGPUEnvForDisplayX11KeepsDisplay(t *testing.T) { | |||
| 55 | 56 | t.Fatalf("X11 environment must inherit DISPLAY: %#v", got.Unset) | |
| 56 | 57 | } | |
| 57 | 58 | } | |
| 59 | + | ||
| 60 | + | func TestWineDebug(t *testing.T) { | |
| 61 | + | for _, tt := range []struct { | |
| 62 | + | name string | |
| 63 | + | value string | |
| 64 | + | set bool | |
| 65 | + | want string | |
| 66 | + | }{ | |
| 67 | + | {name: "default", want: "err+all,warn+waylanddrv"}, | |
| 68 | + | {name: "disabled", set: true, value: "-all", want: "-all"}, | |
| 69 | + | {name: "custom", set: true, value: "+relay", want: "+relay"}, | |
| 70 | + | {name: "empty", set: true}, | |
| 71 | + | } { | |
| 72 | + | t.Run(tt.name, func(t *testing.T) { | |
| 73 | + | t.Setenv("BNETCTL_WINEDEBUG", tt.value) | |
| 74 | + | if !tt.set { | |
| 75 | + | if err := os.Unsetenv("BNETCTL_WINEDEBUG"); err != nil { | |
| 76 | + | t.Fatal(err) | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | for _, driver := range []DisplayDriver{DisplayDriverWayland, DisplayDriverX11} { | |
| 80 | + | if got := BuildGPUEnvForDisplay(driver).Vars["WINEDEBUG"]; got != tt.want { | |
| 81 | + | t.Fatalf("%s: got %q, want %q", driver, got, tt.want) | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | }) | |
| 85 | + | } | |
| 86 | + | } | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package env | 1 | package env | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "os" | |||
| 4 | "slices" | 5 | "slices" | ||
| 5 | "testing" | 6 | "testing" | ||
| 6 | ) | 7 | ) | ||
| @@ -55,3 +56,31 @@ func TestBuildGPUEnvForDisplayX11KeepsDisplay(t *testing.T) { | |||||
| 55 | t.Fatalf("X11 environment must inherit DISPLAY: %#v", got.Unset) | 56 | t.Fatalf("X11 environment must inherit DISPLAY: %#v", got.Unset) | ||
| 56 | } | 57 | } | ||
| 57 | } | 58 | } | ||
| 59 | + | ||||
| 60 | + | func TestWineDebug(t *testing.T) { | |||
| 61 | + | for _, tt := range []struct { | |||
| 62 | + | name string | |||
| 63 | + | value string | |||
| 64 | + | set bool | |||
| 65 | + | want string | |||
| 66 | + | }{ | |||
| 67 | + | {name: "default", want: "err+all,warn+waylanddrv"}, | |||
| 68 | + | {name: "disabled", set: true, value: "-all", want: "-all"}, | |||
| 69 | + | {name: "custom", set: true, value: "+relay", want: "+relay"}, | |||
| 70 | + | {name: "empty", set: true}, | |||
| 71 | + | } { | |||
| 72 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 73 | + | t.Setenv("BNETCTL_WINEDEBUG", tt.value) | |||
| 74 | + | if !tt.set { | |||
| 75 | + | if err := os.Unsetenv("BNETCTL_WINEDEBUG"); err != nil { | |||
| 76 | + | t.Fatal(err) | |||
| 77 | + | } | |||
| 78 | + | } | |||
| 79 | + | for _, driver := range []DisplayDriver{DisplayDriverWayland, DisplayDriverX11} { | |||
| 80 | + | if got := BuildGPUEnvForDisplay(driver).Vars["WINEDEBUG"]; got != tt.want { | |||
| 81 | + | t.Fatalf("%s: got %q, want %q", driver, got, tt.want) | |||
| 82 | + | } | |||
| 83 | + | } | |||
| 84 | + | }) | |||
| 85 | + | } | |||
| 86 | + | } | |||
A internal/adapters/wine/logs.go +123 -0
| @@ -0,0 +1,123 @@ | |||
| 1 | + | package wine | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "os" | |
| 6 | + | "path/filepath" | |
| 7 | + | "sort" | |
| 8 | + | "strings" | |
| 9 | + | "time" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | const ( | |
| 13 | + | wineLogCountLimit = 3 | |
| 14 | + | wineLogTotalBudget int64 = 100 << 20 | |
| 15 | + | wineLogFileLimit int64 = 50 << 20 | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | // launchStderr uses a file descriptor directly: Wine descendants can retain it | |
| 19 | + | // without a copy goroutine delaying cmd.Wait until every descendant exits. | |
| 20 | + | func (a *Adapter) launchStderr() *os.File { | |
| 21 | + | file, err := openWineLog() | |
| 22 | + | if err != nil { | |
| 23 | + | a.log.Warn("cannot create Wine log; using terminal stderr", "error", err) | |
| 24 | + | return os.Stderr | |
| 25 | + | } | |
| 26 | + | a.log.Info("Wine stderr log", "path", file.Name()) | |
| 27 | + | if debug, set := os.LookupEnv("BNETCTL_WINEDEBUG"); set && debug != "err+all,warn+waylanddrv" { | |
| 28 | + | a.log.Warn("custom WINEDEBUG can produce large logs", "path", file.Name()) | |
| 29 | + | } | |
| 30 | + | return file | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | func openWineLog() (*os.File, error) { | |
| 34 | + | state := os.Getenv("XDG_STATE_HOME") | |
| 35 | + | if state == "" { | |
| 36 | + | home, err := os.UserHomeDir() | |
| 37 | + | if err != nil { | |
| 38 | + | return nil, err | |
| 39 | + | } | |
| 40 | + | state = filepath.Join(home, ".local", "state") | |
| 41 | + | } | |
| 42 | + | dir := filepath.Join(state, "bnetctl", "logs") | |
| 43 | + | if err := os.MkdirAll(dir, 0o700); err != nil { | |
| 44 | + | return nil, err | |
| 45 | + | } | |
| 46 | + | // Fixed-width UTC timestamps sort chronologically; the random suffix avoids | |
| 47 | + | // collisions when launches occur together. | |
| 48 | + | file, err := os.CreateTemp(dir, "wine-"+time.Now().UTC().Format("20060102T150405.000000000Z")+"-*.log") | |
| 49 | + | if err != nil { | |
| 50 | + | return nil, err | |
| 51 | + | } | |
| 52 | + | fail := func(err error) (*os.File, error) { | |
| 53 | + | _ = file.Close() | |
| 54 | + | _ = os.Remove(file.Name()) | |
| 55 | + | return nil, err | |
| 56 | + | } | |
| 57 | + | if err := rotateWineLogs(dir, filepath.Base(file.Name())); err != nil { | |
| 58 | + | return fail(err) | |
| 59 | + | } | |
| 60 | + | // Build the replacement beside latest.log, then rename it atomically. | |
| 61 | + | link := file.Name() + ".latest" | |
| 62 | + | if err := os.Symlink(filepath.Base(file.Name()), link); err != nil { | |
| 63 | + | return fail(err) | |
| 64 | + | } | |
| 65 | + | if err := os.Rename(link, filepath.Join(dir, "latest.log")); err != nil { | |
| 66 | + | _ = os.Remove(link) | |
| 67 | + | return fail(err) | |
| 68 | + | } | |
| 69 | + | return file, nil | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | // rotateWineLogs always reserves one slot for the new log, regardless of its | |
| 73 | + | // name or size. Only old files are subject to size limits at launch time. | |
| 74 | + | func rotateWineLogs(dir, newName string) error { | |
| 75 | + | entries, err := os.ReadDir(dir) | |
| 76 | + | if err != nil { | |
| 77 | + | return err | |
| 78 | + | } | |
| 79 | + | var names []string | |
| 80 | + | for _, entry := range entries { | |
| 81 | + | if entry.Name() != newName && entry.Type().IsRegular() && strings.HasPrefix(entry.Name(), "wine-") && strings.HasSuffix(entry.Name(), ".log") { | |
| 82 | + | names = append(names, entry.Name()) | |
| 83 | + | } | |
| 84 | + | } | |
| 85 | + | sort.Sort(sort.Reverse(sort.StringSlice(names))) | |
| 86 | + | var kept []string | |
| 87 | + | for i, name := range names { | |
| 88 | + | path := filepath.Join(dir, name) | |
| 89 | + | info, err := os.Stat(path) | |
| 90 | + | if err != nil { | |
| 91 | + | return err | |
| 92 | + | } | |
| 93 | + | if i >= wineLogCountLimit-1 || info.Size() > wineLogFileLimit { | |
| 94 | + | if err := os.Remove(path); err != nil { | |
| 95 | + | return fmt.Errorf("rotate Wine logs: %w", err) | |
| 96 | + | } | |
| 97 | + | } else { | |
| 98 | + | kept = append(kept, name) | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | return pruneWineLogBudget(dir, kept) | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | // names are newest first. Remove oldest files until the old-log budget fits. | |
| 105 | + | func pruneWineLogBudget(dir string, names []string) error { | |
| 106 | + | sizes := make([]int64, len(names)) | |
| 107 | + | var total int64 | |
| 108 | + | for i, name := range names { | |
| 109 | + | info, err := os.Stat(filepath.Join(dir, name)) | |
| 110 | + | if err != nil { | |
| 111 | + | return err | |
| 112 | + | } | |
| 113 | + | sizes[i] = info.Size() | |
| 114 | + | total += sizes[i] | |
| 115 | + | } | |
| 116 | + | for i := len(names) - 1; i >= 0 && total > wineLogTotalBudget; i-- { | |
| 117 | + | if err := os.Remove(filepath.Join(dir, names[i])); err != nil { | |
| 118 | + | return fmt.Errorf("rotate Wine logs: %w", err) | |
| 119 | + | } | |
| 120 | + | total -= sizes[i] | |
| 121 | + | } | |
| 122 | + | return nil | |
| 123 | + | } | |
| @@ -0,0 +1,123 @@ | |||||
| 1 | + | package wine | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "os" | |||
| 6 | + | "path/filepath" | |||
| 7 | + | "sort" | |||
| 8 | + | "strings" | |||
| 9 | + | "time" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | const ( | |||
| 13 | + | wineLogCountLimit = 3 | |||
| 14 | + | wineLogTotalBudget int64 = 100 << 20 | |||
| 15 | + | wineLogFileLimit int64 = 50 << 20 | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | // launchStderr uses a file descriptor directly: Wine descendants can retain it | |||
| 19 | + | // without a copy goroutine delaying cmd.Wait until every descendant exits. | |||
| 20 | + | func (a *Adapter) launchStderr() *os.File { | |||
| 21 | + | file, err := openWineLog() | |||
| 22 | + | if err != nil { | |||
| 23 | + | a.log.Warn("cannot create Wine log; using terminal stderr", "error", err) | |||
| 24 | + | return os.Stderr | |||
| 25 | + | } | |||
| 26 | + | a.log.Info("Wine stderr log", "path", file.Name()) | |||
| 27 | + | if debug, set := os.LookupEnv("BNETCTL_WINEDEBUG"); set && debug != "err+all,warn+waylanddrv" { | |||
| 28 | + | a.log.Warn("custom WINEDEBUG can produce large logs", "path", file.Name()) | |||
| 29 | + | } | |||
| 30 | + | return file | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | func openWineLog() (*os.File, error) { | |||
| 34 | + | state := os.Getenv("XDG_STATE_HOME") | |||
| 35 | + | if state == "" { | |||
| 36 | + | home, err := os.UserHomeDir() | |||
| 37 | + | if err != nil { | |||
| 38 | + | return nil, err | |||
| 39 | + | } | |||
| 40 | + | state = filepath.Join(home, ".local", "state") | |||
| 41 | + | } | |||
| 42 | + | dir := filepath.Join(state, "bnetctl", "logs") | |||
| 43 | + | if err := os.MkdirAll(dir, 0o700); err != nil { | |||
| 44 | + | return nil, err | |||
| 45 | + | } | |||
| 46 | + | // Fixed-width UTC timestamps sort chronologically; the random suffix avoids | |||
| 47 | + | // collisions when launches occur together. | |||
| 48 | + | file, err := os.CreateTemp(dir, "wine-"+time.Now().UTC().Format("20060102T150405.000000000Z")+"-*.log") | |||
| 49 | + | if err != nil { | |||
| 50 | + | return nil, err | |||
| 51 | + | } | |||
| 52 | + | fail := func(err error) (*os.File, error) { | |||
| 53 | + | _ = file.Close() | |||
| 54 | + | _ = os.Remove(file.Name()) | |||
| 55 | + | return nil, err | |||
| 56 | + | } | |||
| 57 | + | if err := rotateWineLogs(dir, filepath.Base(file.Name())); err != nil { | |||
| 58 | + | return fail(err) | |||
| 59 | + | } | |||
| 60 | + | // Build the replacement beside latest.log, then rename it atomically. | |||
| 61 | + | link := file.Name() + ".latest" | |||
| 62 | + | if err := os.Symlink(filepath.Base(file.Name()), link); err != nil { | |||
| 63 | + | return fail(err) | |||
| 64 | + | } | |||
| 65 | + | if err := os.Rename(link, filepath.Join(dir, "latest.log")); err != nil { | |||
| 66 | + | _ = os.Remove(link) | |||
| 67 | + | return fail(err) | |||
| 68 | + | } | |||
| 69 | + | return file, nil | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | // rotateWineLogs always reserves one slot for the new log, regardless of its | |||
| 73 | + | // name or size. Only old files are subject to size limits at launch time. | |||
| 74 | + | func rotateWineLogs(dir, newName string) error { | |||
| 75 | + | entries, err := os.ReadDir(dir) | |||
| 76 | + | if err != nil { | |||
| 77 | + | return err | |||
| 78 | + | } | |||
| 79 | + | var names []string | |||
| 80 | + | for _, entry := range entries { | |||
| 81 | + | if entry.Name() != newName && entry.Type().IsRegular() && strings.HasPrefix(entry.Name(), "wine-") && strings.HasSuffix(entry.Name(), ".log") { | |||
| 82 | + | names = append(names, entry.Name()) | |||
| 83 | + | } | |||
| 84 | + | } | |||
| 85 | + | sort.Sort(sort.Reverse(sort.StringSlice(names))) | |||
| 86 | + | var kept []string | |||
| 87 | + | for i, name := range names { | |||
| 88 | + | path := filepath.Join(dir, name) | |||
| 89 | + | info, err := os.Stat(path) | |||
| 90 | + | if err != nil { | |||
| 91 | + | return err | |||
| 92 | + | } | |||
| 93 | + | if i >= wineLogCountLimit-1 || info.Size() > wineLogFileLimit { | |||
| 94 | + | if err := os.Remove(path); err != nil { | |||
| 95 | + | return fmt.Errorf("rotate Wine logs: %w", err) | |||
| 96 | + | } | |||
| 97 | + | } else { | |||
| 98 | + | kept = append(kept, name) | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | return pruneWineLogBudget(dir, kept) | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | // names are newest first. Remove oldest files until the old-log budget fits. | |||
| 105 | + | func pruneWineLogBudget(dir string, names []string) error { | |||
| 106 | + | sizes := make([]int64, len(names)) | |||
| 107 | + | var total int64 | |||
| 108 | + | for i, name := range names { | |||
| 109 | + | info, err := os.Stat(filepath.Join(dir, name)) | |||
| 110 | + | if err != nil { | |||
| 111 | + | return err | |||
| 112 | + | } | |||
| 113 | + | sizes[i] = info.Size() | |||
| 114 | + | total += sizes[i] | |||
| 115 | + | } | |||
| 116 | + | for i := len(names) - 1; i >= 0 && total > wineLogTotalBudget; i-- { | |||
| 117 | + | if err := os.Remove(filepath.Join(dir, names[i])); err != nil { | |||
| 118 | + | return fmt.Errorf("rotate Wine logs: %w", err) | |||
| 119 | + | } | |||
| 120 | + | total -= sizes[i] | |||
| 121 | + | } | |||
| 122 | + | return nil | |||
| 123 | + | } | |||
A internal/adapters/wine/logs_test.go +206 -0
| @@ -0,0 +1,206 @@ | |||
| 1 | + | package wine | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "io" | |
| 6 | + | "os" | |
| 7 | + | "path/filepath" | |
| 8 | + | "strings" | |
| 9 | + | "testing" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/bnetctl/internal/domain" | |
| 12 | + | "github.com/charmbracelet/log" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | func TestWineLogRotationAndLatest(t *testing.T) { | |
| 16 | + | state := t.TempDir() | |
| 17 | + | t.Setenv("XDG_STATE_HOME", state) | |
| 18 | + | dir := filepath.Join(state, "bnetctl", "logs") | |
| 19 | + | var paths []string | |
| 20 | + | for i := 0; i < 8; i++ { | |
| 21 | + | file, err := openWineLog() | |
| 22 | + | if err != nil { | |
| 23 | + | t.Fatal(err) | |
| 24 | + | } | |
| 25 | + | paths = append(paths, file.Name()) | |
| 26 | + | if err := file.Close(); err != nil { | |
| 27 | + | t.Fatal(err) | |
| 28 | + | } | |
| 29 | + | target, err := os.Readlink(filepath.Join(dir, "latest.log")) | |
| 30 | + | if err != nil || target != filepath.Base(file.Name()) { | |
| 31 | + | t.Fatalf("latest = %q, %v", target, err) | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | logs, err := filepath.Glob(filepath.Join(dir, "wine-*.log")) | |
| 35 | + | if err != nil || len(logs) != 3 { | |
| 36 | + | t.Fatalf("logs = %v, %v", logs, err) | |
| 37 | + | } | |
| 38 | + | for i, path := range paths { | |
| 39 | + | _, err := os.Stat(path) | |
| 40 | + | if i < 5 && !os.IsNotExist(err) { | |
| 41 | + | t.Fatalf("old log retained: %s (%v)", path, err) | |
| 42 | + | } | |
| 43 | + | if i >= 5 && err != nil { | |
| 44 | + | t.Fatalf("recent log missing: %s (%v)", path, err) | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | func TestWineLogHomeFallback(t *testing.T) { | |
| 50 | + | home := t.TempDir() | |
| 51 | + | t.Setenv("HOME", home) | |
| 52 | + | t.Setenv("XDG_STATE_HOME", "") | |
| 53 | + | file, err := openWineLog() | |
| 54 | + | if err != nil { | |
| 55 | + | t.Fatal(err) | |
| 56 | + | } | |
| 57 | + | t.Cleanup(func() { | |
| 58 | + | if err := file.Close(); err != nil { | |
| 59 | + | t.Error(err) | |
| 60 | + | } | |
| 61 | + | }) | |
| 62 | + | if filepath.Dir(file.Name()) != filepath.Join(home, ".local", "state", "bnetctl", "logs") { | |
| 63 | + | t.Fatal(file.Name()) | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func TestLaunchStderr(t *testing.T) { | |
| 68 | + | for _, async := range []bool{false, true} { | |
| 69 | + | t.Run(map[bool]string{false: "blocking", true: "async"}[async], func(t *testing.T) { | |
| 70 | + | state := t.TempDir() | |
| 71 | + | t.Setenv("XDG_STATE_HOME", state) | |
| 72 | + | script := filepath.Join(t.TempDir(), "wine") | |
| 73 | + | if err := os.WriteFile(script, []byte("#!/bin/sh\nprintf 'driver warning\\n' >&2\n"), 0o755); err != nil { | |
| 74 | + | t.Fatal(err) | |
| 75 | + | } | |
| 76 | + | adapter := NewAdapter(log.New(io.Discard)) | |
| 77 | + | adapter.detected = &domain.WineRuntime{WineBin: script} | |
| 78 | + | if async { | |
| 79 | + | done, err := adapter.RunExeAsync(t.TempDir(), "Battle.net.exe", nil) | |
| 80 | + | if err != nil { | |
| 81 | + | t.Fatal(err) | |
| 82 | + | } | |
| 83 | + | if err := <-done; err != nil { | |
| 84 | + | t.Fatal(err) | |
| 85 | + | } | |
| 86 | + | } else if err := adapter.RunExe(t.TempDir(), "Battle.net.exe", nil); err != nil { | |
| 87 | + | t.Fatal(err) | |
| 88 | + | } | |
| 89 | + | data, err := os.ReadFile(filepath.Join(state, "bnetctl", "logs", "latest.log")) | |
| 90 | + | if err != nil || string(data) != "driver warning\n" { | |
| 91 | + | t.Fatalf("stderr = %q, %v", data, err) | |
| 92 | + | } | |
| 93 | + | }) | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | func TestWineLogFailureFallsBack(t *testing.T) { | |
| 98 | + | state := filepath.Join(t.TempDir(), "not-a-directory") | |
| 99 | + | if err := os.WriteFile(state, nil, 0o600); err != nil { | |
| 100 | + | t.Fatal(err) | |
| 101 | + | } | |
| 102 | + | t.Setenv("XDG_STATE_HOME", state) | |
| 103 | + | adapter := NewAdapter(log.New(io.Discard)) | |
| 104 | + | if got := adapter.launchStderr(); got != os.Stderr { | |
| 105 | + | if err := got.Close(); err != nil { | |
| 106 | + | t.Error(err) | |
| 107 | + | } | |
| 108 | + | t.Fatal("expected terminal fallback") | |
| 109 | + | } | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | func sparseWineLog(t *testing.T, dir, name string, size int64) { | |
| 113 | + | t.Helper() | |
| 114 | + | f, err := os.Create(filepath.Join(dir, name)) | |
| 115 | + | if err != nil { | |
| 116 | + | t.Fatal(err) | |
| 117 | + | } | |
| 118 | + | t.Cleanup(func() { | |
| 119 | + | if err := f.Close(); err != nil { | |
| 120 | + | t.Error(err) | |
| 121 | + | } | |
| 122 | + | }) | |
| 123 | + | if err := f.Truncate(size); err != nil { | |
| 124 | + | t.Fatal(err) | |
| 125 | + | } | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | func TestWineLogBudgetDeletesOldest(t *testing.T) { | |
| 129 | + | dir := t.TempDir() | |
| 130 | + | names := []string{"wine-3.log", "wine-2.log", "wine-1.log"} | |
| 131 | + | for _, name := range names { | |
| 132 | + | sparseWineLog(t, dir, name, 50<<20) | |
| 133 | + | } | |
| 134 | + | if err := pruneWineLogBudget(dir, names); err != nil { | |
| 135 | + | t.Fatal(err) | |
| 136 | + | } | |
| 137 | + | for i, name := range names { | |
| 138 | + | _, err := os.Stat(filepath.Join(dir, name)) | |
| 139 | + | if i == 2 { | |
| 140 | + | if !os.IsNotExist(err) { | |
| 141 | + | t.Fatalf("oldest retained: %v", err) | |
| 142 | + | } | |
| 143 | + | } else if err != nil { | |
| 144 | + | t.Fatal(err) | |
| 145 | + | } | |
| 146 | + | } | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | func TestWineLogOversizedAndNewExemption(t *testing.T) { | |
| 150 | + | dir := t.TempDir() | |
| 151 | + | // The new file sorts oldest and exceeds both limits, but must survive. | |
| 152 | + | sparseWineLog(t, dir, "wine-0.log", 200<<20) | |
| 153 | + | sparseWineLog(t, dir, "wine-3.log", (50<<20)+1) | |
| 154 | + | sparseWineLog(t, dir, "wine-2.log", 50<<20) | |
| 155 | + | sparseWineLog(t, dir, "wine-1.log", 1) | |
| 156 | + | if err := rotateWineLogs(dir, "wine-0.log"); err != nil { | |
| 157 | + | t.Fatal(err) | |
| 158 | + | } | |
| 159 | + | for _, name := range []string{"wine-0.log", "wine-2.log"} { | |
| 160 | + | if _, err := os.Stat(filepath.Join(dir, name)); err != nil { | |
| 161 | + | t.Fatal(err) | |
| 162 | + | } | |
| 163 | + | } | |
| 164 | + | for _, name := range []string{"wine-3.log", "wine-1.log"} { | |
| 165 | + | if _, err := os.Stat(filepath.Join(dir, name)); !os.IsNotExist(err) { | |
| 166 | + | t.Fatalf("log not removed: %s: %v", name, err) | |
| 167 | + | } | |
| 168 | + | } | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | func TestCustomWineDebugWarning(t *testing.T) { | |
| 172 | + | for _, tt := range []struct { | |
| 173 | + | name, value string | |
| 174 | + | set, warn bool | |
| 175 | + | }{ | |
| 176 | + | {name: "unset"}, | |
| 177 | + | {name: "default", set: true, value: "err+all,warn+waylanddrv"}, | |
| 178 | + | {name: "disabled", set: true, value: "-all", warn: true}, | |
| 179 | + | {name: "empty", set: true, warn: true}, | |
| 180 | + | } { | |
| 181 | + | t.Run(tt.name, func(t *testing.T) { | |
| 182 | + | t.Setenv("XDG_STATE_HOME", t.TempDir()) | |
| 183 | + | t.Setenv("BNETCTL_WINEDEBUG", tt.value) | |
| 184 | + | if !tt.set { | |
| 185 | + | if err := os.Unsetenv("BNETCTL_WINEDEBUG"); err != nil { | |
| 186 | + | t.Fatal(err) | |
| 187 | + | } | |
| 188 | + | } | |
| 189 | + | var output bytes.Buffer | |
| 190 | + | adapter := NewAdapter(log.New(&output)) | |
| 191 | + | f := adapter.launchStderr() | |
| 192 | + | t.Cleanup(func() { | |
| 193 | + | if err := f.Close(); err != nil { | |
| 194 | + | t.Error(err) | |
| 195 | + | } | |
| 196 | + | }) | |
| 197 | + | count := strings.Count(output.String(), "custom WINEDEBUG can produce large logs") | |
| 198 | + | if (tt.warn && count != 1) || (!tt.warn && count != 0) { | |
| 199 | + | t.Fatal(output.String()) | |
| 200 | + | } | |
| 201 | + | if tt.warn && !strings.Contains(output.String(), f.Name()) { | |
| 202 | + | t.Fatal("warning missing path") | |
| 203 | + | } | |
| 204 | + | }) | |
| 205 | + | } | |
| 206 | + | } | |
| @@ -0,0 +1,206 @@ | |||||
| 1 | + | package wine | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "io" | |||
| 6 | + | "os" | |||
| 7 | + | "path/filepath" | |||
| 8 | + | "strings" | |||
| 9 | + | "testing" | |||
| 10 | + | ||||
| 11 | + | "github.com/bnema/bnetctl/internal/domain" | |||
| 12 | + | "github.com/charmbracelet/log" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | func TestWineLogRotationAndLatest(t *testing.T) { | |||
| 16 | + | state := t.TempDir() | |||
| 17 | + | t.Setenv("XDG_STATE_HOME", state) | |||
| 18 | + | dir := filepath.Join(state, "bnetctl", "logs") | |||
| 19 | + | var paths []string | |||
| 20 | + | for i := 0; i < 8; i++ { | |||
| 21 | + | file, err := openWineLog() | |||
| 22 | + | if err != nil { | |||
| 23 | + | t.Fatal(err) | |||
| 24 | + | } | |||
| 25 | + | paths = append(paths, file.Name()) | |||
| 26 | + | if err := file.Close(); err != nil { | |||
| 27 | + | t.Fatal(err) | |||
| 28 | + | } | |||
| 29 | + | target, err := os.Readlink(filepath.Join(dir, "latest.log")) | |||
| 30 | + | if err != nil || target != filepath.Base(file.Name()) { | |||
| 31 | + | t.Fatalf("latest = %q, %v", target, err) | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | logs, err := filepath.Glob(filepath.Join(dir, "wine-*.log")) | |||
| 35 | + | if err != nil || len(logs) != 3 { | |||
| 36 | + | t.Fatalf("logs = %v, %v", logs, err) | |||
| 37 | + | } | |||
| 38 | + | for i, path := range paths { | |||
| 39 | + | _, err := os.Stat(path) | |||
| 40 | + | if i < 5 && !os.IsNotExist(err) { | |||
| 41 | + | t.Fatalf("old log retained: %s (%v)", path, err) | |||
| 42 | + | } | |||
| 43 | + | if i >= 5 && err != nil { | |||
| 44 | + | t.Fatalf("recent log missing: %s (%v)", path, err) | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | func TestWineLogHomeFallback(t *testing.T) { | |||
| 50 | + | home := t.TempDir() | |||
| 51 | + | t.Setenv("HOME", home) | |||
| 52 | + | t.Setenv("XDG_STATE_HOME", "") | |||
| 53 | + | file, err := openWineLog() | |||
| 54 | + | if err != nil { | |||
| 55 | + | t.Fatal(err) | |||
| 56 | + | } | |||
| 57 | + | t.Cleanup(func() { | |||
| 58 | + | if err := file.Close(); err != nil { | |||
| 59 | + | t.Error(err) | |||
| 60 | + | } | |||
| 61 | + | }) | |||
| 62 | + | if filepath.Dir(file.Name()) != filepath.Join(home, ".local", "state", "bnetctl", "logs") { | |||
| 63 | + | t.Fatal(file.Name()) | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | func TestLaunchStderr(t *testing.T) { | |||
| 68 | + | for _, async := range []bool{false, true} { | |||
| 69 | + | t.Run(map[bool]string{false: "blocking", true: "async"}[async], func(t *testing.T) { | |||
| 70 | + | state := t.TempDir() | |||
| 71 | + | t.Setenv("XDG_STATE_HOME", state) | |||
| 72 | + | script := filepath.Join(t.TempDir(), "wine") | |||
| 73 | + | if err := os.WriteFile(script, []byte("#!/bin/sh\nprintf 'driver warning\\n' >&2\n"), 0o755); err != nil { | |||
| 74 | + | t.Fatal(err) | |||
| 75 | + | } | |||
| 76 | + | adapter := NewAdapter(log.New(io.Discard)) | |||
| 77 | + | adapter.detected = &domain.WineRuntime{WineBin: script} | |||
| 78 | + | if async { | |||
| 79 | + | done, err := adapter.RunExeAsync(t.TempDir(), "Battle.net.exe", nil) | |||
| 80 | + | if err != nil { | |||
| 81 | + | t.Fatal(err) | |||
| 82 | + | } | |||
| 83 | + | if err := <-done; err != nil { | |||
| 84 | + | t.Fatal(err) | |||
| 85 | + | } | |||
| 86 | + | } else if err := adapter.RunExe(t.TempDir(), "Battle.net.exe", nil); err != nil { | |||
| 87 | + | t.Fatal(err) | |||
| 88 | + | } | |||
| 89 | + | data, err := os.ReadFile(filepath.Join(state, "bnetctl", "logs", "latest.log")) | |||
| 90 | + | if err != nil || string(data) != "driver warning\n" { | |||
| 91 | + | t.Fatalf("stderr = %q, %v", data, err) | |||
| 92 | + | } | |||
| 93 | + | }) | |||
| 94 | + | } | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | func TestWineLogFailureFallsBack(t *testing.T) { | |||
| 98 | + | state := filepath.Join(t.TempDir(), "not-a-directory") | |||
| 99 | + | if err := os.WriteFile(state, nil, 0o600); err != nil { | |||
| 100 | + | t.Fatal(err) | |||
| 101 | + | } | |||
| 102 | + | t.Setenv("XDG_STATE_HOME", state) | |||
| 103 | + | adapter := NewAdapter(log.New(io.Discard)) | |||
| 104 | + | if got := adapter.launchStderr(); got != os.Stderr { | |||
| 105 | + | if err := got.Close(); err != nil { | |||
| 106 | + | t.Error(err) | |||
| 107 | + | } | |||
| 108 | + | t.Fatal("expected terminal fallback") | |||
| 109 | + | } | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | func sparseWineLog(t *testing.T, dir, name string, size int64) { | |||
| 113 | + | t.Helper() | |||
| 114 | + | f, err := os.Create(filepath.Join(dir, name)) | |||
| 115 | + | if err != nil { | |||
| 116 | + | t.Fatal(err) | |||
| 117 | + | } | |||
| 118 | + | t.Cleanup(func() { | |||
| 119 | + | if err := f.Close(); err != nil { | |||
| 120 | + | t.Error(err) | |||
| 121 | + | } | |||
| 122 | + | }) | |||
| 123 | + | if err := f.Truncate(size); err != nil { | |||
| 124 | + | t.Fatal(err) | |||
| 125 | + | } | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | func TestWineLogBudgetDeletesOldest(t *testing.T) { | |||
| 129 | + | dir := t.TempDir() | |||
| 130 | + | names := []string{"wine-3.log", "wine-2.log", "wine-1.log"} | |||
| 131 | + | for _, name := range names { | |||
| 132 | + | sparseWineLog(t, dir, name, 50<<20) | |||
| 133 | + | } | |||
| 134 | + | if err := pruneWineLogBudget(dir, names); err != nil { | |||
| 135 | + | t.Fatal(err) | |||
| 136 | + | } | |||
| 137 | + | for i, name := range names { | |||
| 138 | + | _, err := os.Stat(filepath.Join(dir, name)) | |||
| 139 | + | if i == 2 { | |||
| 140 | + | if !os.IsNotExist(err) { | |||
| 141 | + | t.Fatalf("oldest retained: %v", err) | |||
| 142 | + | } | |||
| 143 | + | } else if err != nil { | |||
| 144 | + | t.Fatal(err) | |||
| 145 | + | } | |||
| 146 | + | } | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | func TestWineLogOversizedAndNewExemption(t *testing.T) { | |||
| 150 | + | dir := t.TempDir() | |||
| 151 | + | // The new file sorts oldest and exceeds both limits, but must survive. | |||
| 152 | + | sparseWineLog(t, dir, "wine-0.log", 200<<20) | |||
| 153 | + | sparseWineLog(t, dir, "wine-3.log", (50<<20)+1) | |||
| 154 | + | sparseWineLog(t, dir, "wine-2.log", 50<<20) | |||
| 155 | + | sparseWineLog(t, dir, "wine-1.log", 1) | |||
| 156 | + | if err := rotateWineLogs(dir, "wine-0.log"); err != nil { | |||
| 157 | + | t.Fatal(err) | |||
| 158 | + | } | |||
| 159 | + | for _, name := range []string{"wine-0.log", "wine-2.log"} { | |||
| 160 | + | if _, err := os.Stat(filepath.Join(dir, name)); err != nil { | |||
| 161 | + | t.Fatal(err) | |||
| 162 | + | } | |||
| 163 | + | } | |||
| 164 | + | for _, name := range []string{"wine-3.log", "wine-1.log"} { | |||
| 165 | + | if _, err := os.Stat(filepath.Join(dir, name)); !os.IsNotExist(err) { | |||
| 166 | + | t.Fatalf("log not removed: %s: %v", name, err) | |||
| 167 | + | } | |||
| 168 | + | } | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | func TestCustomWineDebugWarning(t *testing.T) { | |||
| 172 | + | for _, tt := range []struct { | |||
| 173 | + | name, value string | |||
| 174 | + | set, warn bool | |||
| 175 | + | }{ | |||
| 176 | + | {name: "unset"}, | |||
| 177 | + | {name: "default", set: true, value: "err+all,warn+waylanddrv"}, | |||
| 178 | + | {name: "disabled", set: true, value: "-all", warn: true}, | |||
| 179 | + | {name: "empty", set: true, warn: true}, | |||
| 180 | + | } { | |||
| 181 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 182 | + | t.Setenv("XDG_STATE_HOME", t.TempDir()) | |||
| 183 | + | t.Setenv("BNETCTL_WINEDEBUG", tt.value) | |||
| 184 | + | if !tt.set { | |||
| 185 | + | if err := os.Unsetenv("BNETCTL_WINEDEBUG"); err != nil { | |||
| 186 | + | t.Fatal(err) | |||
| 187 | + | } | |||
| 188 | + | } | |||
| 189 | + | var output bytes.Buffer | |||
| 190 | + | adapter := NewAdapter(log.New(&output)) | |||
| 191 | + | f := adapter.launchStderr() | |||
| 192 | + | t.Cleanup(func() { | |||
| 193 | + | if err := f.Close(); err != nil { | |||
| 194 | + | t.Error(err) | |||
| 195 | + | } | |||
| 196 | + | }) | |||
| 197 | + | count := strings.Count(output.String(), "custom WINEDEBUG can produce large logs") | |||
| 198 | + | if (tt.warn && count != 1) || (!tt.warn && count != 0) { | |||
| 199 | + | t.Fatal(output.String()) | |||
| 200 | + | } | |||
| 201 | + | if tt.warn && !strings.Contains(output.String(), f.Name()) { | |||
| 202 | + | t.Fatal("warning missing path") | |||
| 203 | + | } | |||
| 204 | + | }) | |||
| 205 | + | } | |||
| 206 | + | } | |||
M internal/adapters/wine/runtime.go +11 -2
| @@ -213,7 +213,11 @@ func (a *Adapter) RunExe(prefixPath string, exePath string, wineEnv *domain.Wine | |||
| 213 | 213 | cmd := exec.Command(rt.WineBin, exePath) | |
| 214 | 214 | cmd.Env = envMapToSlice(env) | |
| 215 | 215 | cmd.Stdout = os.Stdout | |
| 216 | - | cmd.Stderr = os.Stderr | |
| 216 | + | stderr := a.launchStderr() | |
| 217 | + | if stderr != os.Stderr { | |
| 218 | + | defer func() { _ = stderr.Close() }() | |
| 219 | + | } | |
| 220 | + | cmd.Stderr = stderr | |
| 217 | 221 | return cmd.Run() | |
| 218 | 222 | } | |
| 219 | 223 | ||
| @@ -234,7 +238,12 @@ func (a *Adapter) RunExeAsync(prefixPath string, exePath string, wineEnv *domain | |||
| 234 | 238 | cmd := exec.Command(rt.WineBin, cmdArgs...) | |
| 235 | 239 | cmd.Env = envMapToSlice(env) | |
| 236 | 240 | cmd.Stdout = os.Stdout | |
| 237 | - | cmd.Stderr = os.Stderr | |
| 241 | + | stderr := a.launchStderr() | |
| 242 | + | if stderr != os.Stderr { | |
| 243 | + | // Start duplicates the descriptor into the child before we close it. | |
| 244 | + | defer func() { _ = stderr.Close() }() | |
| 245 | + | } | |
| 246 | + | cmd.Stderr = stderr | |
| 238 | 247 | ||
| 239 | 248 | if err := cmd.Start(); err != nil { | |
| 240 | 249 | return nil, fmt.Errorf("start process: %w", err) | |
| @@ -213,7 +213,11 @@ func (a *Adapter) RunExe(prefixPath string, exePath string, wineEnv *domain.Wine | |||||
| 213 | cmd := exec.Command(rt.WineBin, exePath) | 213 | cmd := exec.Command(rt.WineBin, exePath) | ||
| 214 | cmd.Env = envMapToSlice(env) | 214 | cmd.Env = envMapToSlice(env) | ||
| 215 | cmd.Stdout = os.Stdout | 215 | cmd.Stdout = os.Stdout | ||
| 216 | - | cmd.Stderr = os.Stderr | 216 | + | stderr := a.launchStderr() |
| 217 | + | if stderr != os.Stderr { | |||
| 218 | + | defer func() { _ = stderr.Close() }() | |||
| 219 | + | } | |||
| 220 | + | cmd.Stderr = stderr | |||
| 217 | return cmd.Run() | 221 | return cmd.Run() | ||
| 218 | } | 222 | } | ||
| 219 | 223 | ||||
| @@ -234,7 +238,12 @@ func (a *Adapter) RunExeAsync(prefixPath string, exePath string, wineEnv *domain | |||||
| 234 | cmd := exec.Command(rt.WineBin, cmdArgs...) | 238 | cmd := exec.Command(rt.WineBin, cmdArgs...) | ||
| 235 | cmd.Env = envMapToSlice(env) | 239 | cmd.Env = envMapToSlice(env) | ||
| 236 | cmd.Stdout = os.Stdout | 240 | cmd.Stdout = os.Stdout | ||
| 237 | - | cmd.Stderr = os.Stderr | 241 | + | stderr := a.launchStderr() |
| 242 | + | if stderr != os.Stderr { | |||
| 243 | + | // Start duplicates the descriptor into the child before we close it. | |||
| 244 | + | defer func() { _ = stderr.Close() }() | |||
| 245 | + | } | |||
| 246 | + | cmd.Stderr = stderr | |||
| 238 | 247 | ||||
| 239 | if err := cmd.Start(); err != nil { | 248 | if err := cmd.Start(); err != nil { | ||
| 240 | return nil, fmt.Errorf("start process: %w", err) | 249 | return nil, fmt.Errorf("start process: %w", err) | ||
Comments00 unresolved
no comments yet.