Skip to content

Commit ab7b7e4

Browse files
committed
feat(cli): patch the update endpoint on macOS when blocking updates
1 parent 60770e4 commit ab7b7e4

1 file changed

Lines changed: 68 additions & 4 deletions

File tree

src/cmd/block-updates.go

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"bytes"
5+
"fmt"
56
"os"
67
"os/exec"
78
"path/filepath"
@@ -33,18 +34,24 @@ func BlockSpotifyUpdates(disabled bool) {
3334
return
3435
}
3536
updateDir := homeDir + "/Library/Application Support/Spotify/PersistentCache/Update"
37+
exec.Command("pkill", "Spotify").Run()
38+
exec.Command("mkdir", "-p", updateDir).Run()
3639
if disabled {
37-
exec.Command("pkill", "Spotify").Run()
38-
exec.Command("mkdir", "-p", updateDir).Run()
3940
exec.Command("chflags", "uchg", updateDir).Run()
4041
msg = "Disabled"
4142
} else {
42-
exec.Command("pkill", "Spotify").Run()
43-
exec.Command("mkdir", "-p", updateDir).Run()
4443
exec.Command("chflags", "nouchg", updateDir).Run()
4544
msg = "Enabled"
4645
}
4746

47+
// chflags alone is not enough anymore: current clients stage updates
48+
// via a segmented downloader that does not reference that directory.
49+
// Patching the update endpoint makes the updater unreachable
50+
// regardless of how the payload is fetched.
51+
if err := patchDarwinUpdateEndpoint(spotifyExecPath, disabled); err != nil {
52+
utils.PrintWarning("Endpoint patch failed (lock still applied): " + err.Error())
53+
}
54+
4855
utils.PrintSuccess(msg + " Spotify updates!")
4956
return
5057
}
@@ -75,3 +82,60 @@ func BlockSpotifyUpdates(disabled bool) {
7582
file.WriteAt([]byte(str), int64(i+15))
7683
utils.PrintSuccess(msg + " Spotify updates!")
7784
}
85+
86+
const (
87+
darwinUpdateEndpoint = "desktop-update/v2/update"
88+
darwinUpdateEndpointPatched = "desktop-update/no/thanks"
89+
darwinUpdateEndpointPatchOff = len("desktop-update/")
90+
)
91+
92+
// patchDarwinUpdateEndpoint rewrites the desktop-update endpoint inside the
93+
// Spotify binary and re-signs the bundle ad-hoc so it still launches on
94+
// Apple Silicon. Blocking writes "no/thanks" over "v2/update"; unblocking
95+
// restores the original bytes (from the backup taken on first block, or by
96+
// reversing the patch).
97+
func patchDarwinUpdateEndpoint(binaryPath string, block bool) error {
98+
raw, err := os.ReadFile(binaryPath)
99+
if err != nil {
100+
return err
101+
}
102+
103+
backupPath := filepath.Join(utils.GetSpicetifyFolder(), "spotify-binary-backup")
104+
105+
if block {
106+
if !bytes.Contains(raw, []byte(darwinUpdateEndpoint)) {
107+
// Already patched (or the endpoint moved): nothing to do.
108+
return nil
109+
}
110+
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
111+
if err := os.WriteFile(backupPath, raw, 0755); err != nil {
112+
return fmt.Errorf("cannot back up binary: %w", err)
113+
}
114+
}
115+
idx := bytes.Index(raw, []byte(darwinUpdateEndpoint))
116+
copy(raw[idx+darwinUpdateEndpointPatchOff:], "no/thanks")
117+
} else {
118+
if st, err := os.Stat(backupPath); err == nil && !st.IsDir() {
119+
raw, err = os.ReadFile(backupPath)
120+
if err != nil {
121+
return err
122+
}
123+
} else {
124+
idx := bytes.Index(raw, []byte(darwinUpdateEndpointPatched))
125+
if idx < 0 {
126+
return fmt.Errorf("patched endpoint not found and no backup to restore")
127+
}
128+
copy(raw[idx+darwinUpdateEndpointPatchOff:], "v2/update")
129+
}
130+
}
131+
132+
if err := os.WriteFile(binaryPath, raw, 0755); err != nil {
133+
return err
134+
}
135+
136+
bundlePath := filepath.Join(binaryPath, "..", "..", "..")
137+
if out, err := exec.Command("codesign", "--force", "--deep", "--sign", "-", bundlePath).CombinedOutput(); err != nil {
138+
return fmt.Errorf("codesign failed: %w (%s)", err, strings.TrimSpace(string(out)))
139+
}
140+
return nil
141+
}

0 commit comments

Comments
 (0)