diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 030433b..c33bb77 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,9 +15,12 @@ jobs: env: # Optional code signing (docs/SIGNING.md). Without these the build is # unsigned; everything else works the same. SignPath signs tagged - # builds; a .pfx (SIGN_PFX_*) signs every build. + # builds; a .pfx (SIGN_PFX_*) signs pushes to main and tags. Pull + # requests are never signed: Dependabot's and forks' runs have no + # secrets, and the job has to pass for them too. SIGN_PFX_BASE64: ${{ secrets.SIGN_PFX_BASE64 }} SIGN_PFX_PASSWORD: ${{ secrets.SIGN_PFX_PASSWORD }} + SIGN: ${{ github.event_name != 'pull_request' }} SIGNPATH: ${{ secrets.SIGNPATH_API_TOKEN != '' && startsWith(github.ref, 'refs/tags/v') }} steps: - uses: actions/checkout@v7 @@ -71,6 +74,15 @@ jobs: - name: Build run: wails3 build VERSION=${{ env.WL_VERSION }} + - name: Vet + run: go vet ./... + + - name: Lint + # Pinned like Wails; bump on purpose. The config is .golangci.yml. + uses: golangci/golangci-lint-action@v9 + with: + version: v2.15.0 + - name: Test run: go test ./... @@ -102,7 +114,7 @@ jobs: } - name: Sign - if: env.SIGNPATH != 'true' + if: env.SIGN == 'true' && env.SIGNPATH != 'true' run: ./build/windows/sign.ps1 bin/Seaglass.exe - name: WebView2 bootstrapper @@ -148,12 +160,12 @@ jobs: if ($env:SIGNPATH -eq 'true') { Copy-Item signed-binaries/Seaglass.exe, signed-binaries/uninstall.exe bin/ -Force $defs += "-DSIGNED_UNINSTALLER=$pwd\bin\uninstall.exe" - } elseif ($env:SIGN_PFX_BASE64) { + } elseif ($env:SIGN -eq 'true' -and $env:SIGN_PFX_BASE64) { $defs += "-DSIGN_CMD=powershell -NoProfile -ExecutionPolicy Bypass -File $pwd\build\windows\sign.ps1" } & $env:MAKENSIS -V2 @defs build/windows/nsis/project.nsi if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - if ($env:SIGNPATH -ne 'true') { ./build/windows/sign.ps1 bin/Seaglass-setup.exe } + if ($env:SIGN -eq 'true' -and $env:SIGNPATH -ne 'true') { ./build/windows/sign.ps1 bin/Seaglass-setup.exe } - name: Upload installer for signing id: unsigned-installer diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..9bca560 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,35 @@ +# golangci-lint (v2 config; the version CI runs is pinned in +# .github/workflows/build.yml). Locally: golangci-lint run ./internal/... ./tools/... +# (the root package needs frontend/dist from a build). +# +# errcheck and gosec are not on yet: the first run reported 189 and 259 +# findings. They get turned on once those are worked through; see the +# follow-ups in the PR that added this file. +version: "2" + +linters: + default: none + enable: + - govet + - ineffassign + - misspell + - staticcheck + - unused + settings: + staticcheck: + # golangci-lint's default set, minus ST1005: error strings here start + # with product names (Steam, Syncer, Epic, Seaglass) and are shown to + # the user as they are. + checks: + - all + - -ST1000 + - -ST1003 + - -ST1005 + - -ST1016 + - -ST1020 + - -ST1021 + - -ST1022 + +formatters: + enable: + - gofmt diff --git a/internal/achievements/model.go b/internal/achievements/model.go index f7759f3..6a57ccf 100644 --- a/internal/achievements/model.go +++ b/internal/achievements/model.go @@ -53,7 +53,7 @@ type List struct { Partial bool `json:"-"` } -// FixUplayINI: Seaglass can turn achievements on in the game's Uplay +// FixUplayINI marks that Seaglass can turn achievements on in the game's Uplay // emulator ini (EnableUplay). const FixUplayINI = "uplay-ini" diff --git a/internal/achievements/steamlocal.go b/internal/achievements/steamlocal.go index d7b90a8..37dfdc4 100644 --- a/internal/achievements/steamlocal.go +++ b/internal/achievements/steamlocal.go @@ -33,7 +33,7 @@ func validLang(s string) bool { return false } for _, r := range s { - if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r == '_') { + if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && r != '_' { return false } } diff --git a/internal/app/dev.go b/internal/app/dev.go index e6dc00b..ddfd0c3 100644 --- a/internal/app/dev.go +++ b/internal/app/dev.go @@ -19,7 +19,7 @@ import ( "golang.org/x/sys/windows" ) -// Dev flags drive the real app from a test harness (tools/harness). They +// DevArgs are flags that drive the real app from a test harness (tools/harness). They // only work in dev builds: a release build ignores them. // // --virtual-pad[=ps|xbox] plug in a virtual controller (SDL's own) diff --git a/internal/app/diagnostics_test.go b/internal/app/diagnostics_test.go index f524fb1..e689f3a 100644 --- a/internal/app/diagnostics_test.go +++ b/internal/app/diagnostics_test.go @@ -19,7 +19,7 @@ func TestCrashCapture(t *testing.T) { captureCrashes(os.Getenv("WL_CRASH_DIR")) go func() { // outside the test's own recover var m map[string]int - m["boom"] = 1 // nil map: an unhandled panic + m["boom"] = 1 //nolint:staticcheck // nil map: an unhandled panic, which is the point of this test }() // Wait for the crash to end the process. Nothing may signal back // from the panicking goroutine: its deferred calls run while the diff --git a/internal/app/launch_test.go b/internal/app/launch_test.go index bbce2f8..be40e60 100644 --- a/internal/app/launch_test.go +++ b/internal/app/launch_test.go @@ -119,13 +119,13 @@ func TestGameForPath(t *testing.T) { {ID: 5, Title: "Too broad", Installed: true, Dir: `C:\Program Files`}, } for path, want := range map[string]int64{ - `D:\Games\Hades\x64\Hades.exe`: 2, // the most specific folder - `D:\Games\Other\Other.exe`: 1, - `D:\Games\Gone\Gone.exe`: 1, // not installed: only the folder around it counts - `C:\Tools\Editor\editor.exe`: 0, // hidden - `C:\Program Files\Some\app.exe`: 0, // too broad a folder - `D:\Games\Hades\Seaglass.exe`: 0, - `C:\Windows\System32\notepad.exe`: 0, + `D:\Games\Hades\x64\Hades.exe`: 2, // the most specific folder + `D:\Games\Other\Other.exe`: 1, + `D:\Games\Gone\Gone.exe`: 1, // not installed: only the folder around it counts + `C:\Tools\Editor\editor.exe`: 0, // hidden + `C:\Program Files\Some\app.exe`: 0, // too broad a folder + `D:\Games\Hades\Seaglass.exe`: 0, + `C:\Windows\System32\notepad.exe`: 0, } { g, ok := gameForPath(games, path) if got := map[bool]int64{true: g.ID, false: 0}[ok]; got != want { diff --git a/internal/app/metadata.go b/internal/app/metadata.go index 0ae32eb..de68a2b 100644 --- a/internal/app/metadata.go +++ b/internal/app/metadata.go @@ -75,7 +75,7 @@ func (w *metaWorker) queueMissing() { now := time.Now() w.mu.Lock() for _, g := range games { - if !(g.Installed || showOwned && g.Owned) || g.Hidden || w.queued[g.ID] { + if !g.Installed && (!showOwned || !g.Owned) || g.Hidden || w.queued[g.ID] { continue } if t, ok := w.failed[g.ID]; ok && now.Sub(t) < metaRetryGap { diff --git a/internal/app/profile.go b/internal/app/profile.go index 6696149..9de7df7 100644 --- a/internal/app/profile.go +++ b/internal/app/profile.go @@ -82,7 +82,6 @@ type profileState struct { mu sync.Mutex info Profile unl map[string]map[string]int64 // merged achievements by game key - pcs string // PC the settings came from data time.Time // last launcherData call that worked kick chan struct{} } diff --git a/internal/emulated/cemu.go b/internal/emulated/cemu.go index ecce56b..c6fad21 100644 --- a/internal/emulated/cemu.go +++ b/internal/emulated/cemu.go @@ -145,7 +145,7 @@ func isTitleID(s string) bool { return false } for _, r := range strings.ToLower(s) { - if !(r >= '0' && r <= '9' || r >= 'a' && r <= 'f') { + if (r < '0' || r > '9') && (r < 'a' || r > 'f') { return false } } diff --git a/internal/emulated/emulated.go b/internal/emulated/emulated.go index a34570b..049bbc3 100644 --- a/internal/emulated/emulated.go +++ b/internal/emulated/emulated.go @@ -563,7 +563,7 @@ func isDiscID(s string) bool { return false } for _, r := range s { - if !(r >= 'A' && r <= 'Z' || r >= '0' && r <= '9') { + if (r < 'A' || r > 'Z') && (r < '0' || r > '9') { return false } } diff --git a/internal/emulated/rpcs3.go b/internal/emulated/rpcs3.go index 126e4ed..8397c7c 100644 --- a/internal/emulated/rpcs3.go +++ b/internal/emulated/rpcs3.go @@ -61,7 +61,7 @@ var rpcs3 = &emulator{ return games, roots }, folder: func(in *install, dir string, names map[string]bool) (Game, bool) { - if !names["ps3_game"] && !(names["param.sfo"] && names["usrdir"]) { + if !names["ps3_game"] && (!names["param.sfo"] || !names["usrdir"]) { return Game{}, false } return ps3Game(dir) @@ -118,7 +118,7 @@ func isSerial(s string) bool { return false } for i, r := range s { - if i < 4 && !(r >= 'A' && r <= 'Z') || i >= 4 && !(r >= '0' && r <= '9') { + if i < 4 && (r < 'A' || r > 'Z') || i >= 4 && (r < '0' || r > '9') { return false } } diff --git a/internal/launch/launch.go b/internal/launch/launch.go index 961a424..9d56d8a 100644 --- a/internal/launch/launch.go +++ b/internal/launch/launch.go @@ -502,7 +502,7 @@ func (m *Manager) follow(ctx context.Context, p Plan, pid uint32, before map[uin } secs := int64(total) m.update(func(s *Session) { s.Seconds = secs }) - case now.Sub(began) > timeout && !(starting && now.Sub(began) <= starterTimeout): + case now.Sub(began) > timeout && (!starting || now.Sub(began) > starterTimeout): m.update(func(s *Session) { s.Phase = Ended s.Note = "Seaglass couldn't see the game running, so no playtime was counted." @@ -512,7 +512,6 @@ func (m *Manager) follow(ctx context.Context, p Plan, pid uint32, before map[uin } select { case <-m.stop: - total += gap unsaved += gap flush() return -1 diff --git a/internal/meta/images.go b/internal/meta/images.go index c363320..3c7ae72 100644 --- a/internal/meta/images.go +++ b/internal/meta/images.go @@ -434,7 +434,7 @@ func PruneArt(artDir string, keep map[string]bool, grace time.Duration) (removed cutoff := time.Now().Add(-grace) for _, e := range entries { name := e.Name() - if e.IsDir() || keep[artPrefix+name] || !(reArtName.MatchString(name) || strings.HasSuffix(name, ".tmp")) { + if e.IsDir() || keep[artPrefix+name] || (!reArtName.MatchString(name) && !strings.HasSuffix(name, ".tmp")) { continue } fi, err := e.Info() diff --git a/internal/owned/achievements_test.go b/internal/owned/achievements_test.go index 84c5de3..3111c8f 100644 --- a/internal/owned/achievements_test.go +++ b/internal/owned/achievements_test.go @@ -156,15 +156,15 @@ func TestGOGCode(t *testing.T) { func TestGOGAchievementsAPI(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.URL.Path == "/token": + switch r.URL.Path { + case "/token": q := r.URL.Query() if q.Get("client_id") != gogClientID || q.Get("grant_type") != "refresh_token" || q.Get("refresh_token") != "r1" { w.WriteHeader(400) return } w.Write([]byte(`{"access_token":"a2","refresh_token":"r2","user_id":"u1","expires_in":3600}`)) - case r.URL.Path == "/clients/5134/users/u1/achievements": + case "/clients/5134/users/u1/achievements": if r.Header.Get("Authorization") != "Bearer a2" { w.WriteHeader(401) return diff --git a/internal/platform/foreground.go b/internal/platform/foreground.go index d073706..5eb8155 100644 --- a/internal/platform/foreground.go +++ b/internal/platform/foreground.go @@ -19,7 +19,7 @@ var ( procTranslateMessage = user32.NewProc("TranslateMessage") procDispatchMessageW = user32.NewProc("DispatchMessageW") procPostThreadMessageW = user32.NewProc("PostThreadMessageW") - procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId") + procGetWindowThreadProcessID = user32.NewProc("GetWindowThreadProcessId") procGetForegroundWindow = user32.NewProc("GetForegroundWindow") ) @@ -30,7 +30,7 @@ func ForegroundPID() uint32 { return 0 } var pid uint32 - procGetWindowThreadProcessId.Call(h, uintptr(unsafe.Pointer(&pid))) + procGetWindowThreadProcessID.Call(h, uintptr(unsafe.Pointer(&pid))) return pid } @@ -51,7 +51,7 @@ var ( func foregroundCallback(hook, event, hwnd, idObject, idChild, thread, when uintptr) uintptr { var pid uint32 - procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&pid))) + procGetWindowThreadProcessID.Call(hwnd, uintptr(unsafe.Pointer(&pid))) fgMu.Lock() fn := fgFn fgMu.Unlock() diff --git a/internal/platform/signer_test.go b/internal/platform/signer_test.go index 38d834a..4de4888 100644 --- a/internal/platform/signer_test.go +++ b/internal/platform/signer_test.go @@ -43,8 +43,8 @@ func TestSigner(t *testing.T) { func TestCommandExe(t *testing.T) { for in, want := range map[string]string{ `"C:\Program Files\Seaglass\Seaglass.exe" --tray`: `C:\Program Files\Seaglass\Seaglass.exe`, - `C:\WL\Seaglass.exe --tray`: `C:\WL\Seaglass.exe`, - `"unterminated`: ``, + `C:\WL\Seaglass.exe --tray`: `C:\WL\Seaglass.exe`, + `"unterminated`: ``, } { if got := commandExe(in); got != want { t.Errorf("commandExe(%q) = %q, want %q", in, got, want) diff --git a/internal/platform/window.go b/internal/platform/window.go index dffe662..0498a34 100644 --- a/internal/platform/window.go +++ b/internal/platform/window.go @@ -57,7 +57,7 @@ func BringToFront(hwnd uintptr) bool { if fg == 0 { return false } - fgThread, _, _ := procGetWindowThreadProcessId.Call(fg, 0) + fgThread, _, _ := procGetWindowThreadProcessID.Call(fg, 0) self := uintptr(windows.GetCurrentThreadId()) if fgThread != 0 && fgThread != self { procAttachThreadInput.Call(self, fgThread, 1) diff --git a/internal/scan/steam_test.go b/internal/scan/steam_test.go index 2a725f5..4efa61a 100644 --- a/internal/scan/steam_test.go +++ b/internal/scan/steam_test.go @@ -16,7 +16,7 @@ func TestSteamCandidatesOldLibraryFormat(t *testing.T) { "steamapps/libraryfolders.vdf": `"LibraryFolders" { "TimeNextStatsReport" "1" "1" "` + vdfPath + `" }`, }) mk(t, lib, map[string]string{ - "steamapps/appmanifest_413150.acf": `"AppState" { "appid" "413150" "name" "Stardew Valley" "StateFlags" "4" "installdir" "Stardew Valley" }`, + "steamapps/appmanifest_413150.acf": `"AppState" { "appid" "413150" "name" "Stardew Valley" "StateFlags" "4" "installdir" "Stardew Valley" }`, "steamapps/common/Stardew Valley/Stardew Valley.exe": "x", }) libs := SteamLibraries(root) diff --git a/internal/syncer/client.go b/internal/syncer/client.go index 37bce0c..2fd0316 100644 --- a/internal/syncer/client.go +++ b/internal/syncer/client.go @@ -250,10 +250,10 @@ func checkServer(conn net.Conn) error { return nil } -var procGetNamedPipeServerProcessId = windows.NewLazySystemDLL("kernel32.dll").NewProc("GetNamedPipeServerProcessId") +var procGetNamedPipeServerProcessID = windows.NewLazySystemDLL("kernel32.dll").NewProc("GetNamedPipeServerProcessId") func getNamedPipeServerProcessID(h windows.Handle, pid *uint32) error { - r, _, err := procGetNamedPipeServerProcessId.Call(uintptr(h), uintptr(unsafe.Pointer(pid))) + r, _, err := procGetNamedPipeServerProcessID.Call(uintptr(h), uintptr(unsafe.Pointer(pid))) if r == 0 { return err } diff --git a/internal/update/update_test.go b/internal/update/update_test.go index 29d2a8e..e02b62f 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -88,9 +88,7 @@ func (f *fakeGitHub) publish(tag string, files map[string][]byte, extra ...map[s f.files[name] = b assets = append(assets, map[string]any{"name": name, "size": len(b), "browser_download_url": f.srv.URL + "/download/" + name}) } - for _, e := range extra { - assets = append(assets, e) - } + assets = append(assets, extra...) f.release = map[string]any{"tag_name": tag, "body": "notes", "html_url": "https://github.com/ApolloF/Seaglass/releases/tag/" + tag, "assets": assets} } diff --git a/internal/vpad/vigem.go b/internal/vpad/vigem.go index e5a837a..064cc97 100644 --- a/internal/vpad/vigem.go +++ b/internal/vpad/vigem.go @@ -117,7 +117,7 @@ func NewX360() (*X360, error) { if err != nil || len(paths) == 0 { return nil, ErrNoViGEm } - var last error = ErrNoViGEm + last := ErrNoViGEm for _, p := range paths { h, err := openBus(p) if err != nil { diff --git a/main.go b/main.go index bc81615..753cb06 100644 --- a/main.go +++ b/main.go @@ -44,7 +44,7 @@ func main() { platform.WaitInstanceGone(oldUniqueID, 15*time.Second) } var moved []string - if !(dev && args.Dev.DataDir != "") && !platform.InstanceRunning(uniqueID) { + if (!dev || args.Dev.DataDir == "") && !platform.InstanceRunning(uniqueID) { // WaterLauncher's library and settings. moved = platform.MoveOldData(platform.InstanceRunning(oldUniqueID)) } diff --git a/tools/release/cut.go b/tools/release/cut.go index 574eb65..ef3e54f 100644 --- a/tools/release/cut.go +++ b/tools/release/cut.go @@ -15,17 +15,17 @@ import ( // cut releases a version from main, one checked step at a time; the first // failing check stops it before anything that can't be undone. // -// go run ./tools/release cut v1.2.0 [--pr 12] [--dry-run] +// go run ./tools/release cut v1.2.0 [--pr 12] [--dry-run] // -// 1. This PC's release key is one the updater trusts. -// 2. The tag is a version, its notes exist, it isn't used yet. -// 3. The working tree is clean, on main, level with origin/main. -// 4. With --pr: that pull request is merged and its merge commit is in main. -// 5. CI passed for main's head (waits while it runs). -// 6. "Version X.Y.Z" (build/windows/info.json) goes into main through a -// pull request, since main takes nothing else; skipped if already there. -// 7. The tag is pushed; CI builds and tests it and makes a draft release. -// 8. publish signs the draft with the release key and makes it public. +// 1. This PC's release key is one the updater trusts. +// 2. The tag is a version, its notes exist, it isn't used yet. +// 3. The working tree is clean, on main, level with origin/main. +// 4. With --pr: that pull request is merged and its merge commit is in main. +// 5. CI passed for main's head (waits while it runs). +// 6. "Version X.Y.Z" (build/windows/info.json) goes into main through a +// pull request, since main takes nothing else; skipped if already there. +// 7. The tag is pushed; CI builds and tests it and makes a draft release. +// 8. publish signs the draft with the release key and makes it public. func cut(tag string, pr string, dry bool) error { num, err := versionNumber(tag) if err != nil {