Skip to content

Commit b566950

Browse files
committed
feat: brew-aware install with auto service registration on macOS
- Detect Homebrew-managed binary and register launchd agent pointing at brew path directly, so brew upgrade keeps the service current - Auto-register and start launchd agent in cask post-install hook - Auto-unregister in cask post-uninstall hook - Use platform-correct terminology (launchd user agent vs systemd user service)
1 parent 1ef8d77 commit b566950

File tree

3 files changed

+111
-15
lines changed

3 files changed

+111
-15
lines changed

.goreleaser.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,18 @@ homebrew_casks:
8686
install: |
8787
if OS.mac?
8888
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/greyproxy"]
89+
system_command "#{staged_path}/greyproxy", args: ["install", "-f"]
90+
end
91+
uninstall: |
92+
if OS.mac?
93+
system_command "#{staged_path}/greyproxy", args: ["uninstall", "-f"]
8994
end
9095
dependencies:
9196
- formula: terminal-notifier
9297
caveats: |
98+
greyproxy has been installed and started as a launchd user agent.
99+
Dashboard: http://localhost:43080
100+
93101
For desktop notifications on macOS, install terminal-notifier:
94102
brew install terminal-notifier
95103

cmd/greyproxy/install.go

Lines changed: 102 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import (
44
"fmt"
55
"io"
66
"os"
7+
"os/exec"
78
"path/filepath"
9+
"runtime"
10+
"strings"
811

912
"github.com/kardianos/service"
1013
flag "github.com/spf13/pflag"
@@ -27,19 +30,49 @@ func installBinPath() string {
2730
return filepath.Join(home, ".local", "bin", "greyproxy")
2831
}
2932

30-
func newServiceControl() (service.Service, error) {
31-
binDst := installBinPath()
32-
svcConfig := &service.Config{
33+
func serviceLabel() string {
34+
if runtime.GOOS == "darwin" {
35+
return "launchd user agent"
36+
}
37+
return "systemd user service"
38+
}
39+
40+
// isBrewManaged returns true if the given binary path lives under the
41+
// Homebrew prefix (e.g. /opt/homebrew or /usr/local).
42+
func isBrewManaged(binPath string) bool {
43+
if runtime.GOOS != "darwin" {
44+
return false
45+
}
46+
out, err := exec.Command("brew", "--prefix").Output()
47+
if err != nil {
48+
return false
49+
}
50+
prefix := strings.TrimSpace(string(out))
51+
if prefix == "" {
52+
return false
53+
}
54+
return strings.HasPrefix(binPath, prefix)
55+
}
56+
57+
func newServiceConfig(execPath string) *service.Config {
58+
return &service.Config{
3359
Name: serviceName,
3460
DisplayName: "Greyproxy",
3561
Description: "Greyproxy network proxy service",
36-
Executable: binDst,
62+
Executable: execPath,
3763
Arguments: []string{"serve"},
3864
Option: service.KeyValue{
3965
"UserService": true,
4066
},
4167
}
42-
return service.New(&program{}, svcConfig)
68+
}
69+
70+
func newServiceControl() (service.Service, error) {
71+
return service.New(&program{}, newServiceConfig(installBinPath()))
72+
}
73+
74+
func newServiceControlAt(execPath string) (service.Service, error) {
75+
return service.New(&program{}, newServiceConfig(execPath))
4376
}
4477

4578
func isInstalled() bool {
@@ -49,7 +82,6 @@ func isInstalled() bool {
4982

5083
func handleInstall(args []string) {
5184
force := parseInstallFlags(args)
52-
binDst := installBinPath()
5385

5486
binSrc, err := os.Executable()
5587
if err != nil {
@@ -58,14 +90,25 @@ func handleInstall(args []string) {
5890
}
5991
binSrc, _ = filepath.EvalSymlinks(binSrc)
6092

93+
// When installed via Homebrew, skip the binary copy and register the
94+
// service pointing at the brew-managed binary directly. This way
95+
// "brew upgrade" keeps the running service up to date.
96+
if isBrewManaged(binSrc) {
97+
handleBrewInstall(binSrc, force)
98+
return
99+
}
100+
101+
binDst := installBinPath()
102+
61103
if isInstalled() {
62104
handleReinstall(binSrc, binDst, force)
63105
return
64106
}
65107

108+
label := serviceLabel()
66109
fmt.Printf("Ready to install greyproxy. This will:\n")
67110
fmt.Printf(" 1. Copy %s -> %s\n", binSrc, binDst)
68-
fmt.Printf(" 2. Install greyproxy as a systemd user service\n")
111+
fmt.Printf(" 2. Register greyproxy as a %s\n", label)
69112
fmt.Printf(" 3. Start the service\n")
70113

71114
if !force {
@@ -81,13 +124,55 @@ func handleInstall(args []string) {
81124
fmt.Println("\nDashboard: http://localhost:43080")
82125
}
83126

127+
func handleBrewInstall(brewBin string, force bool) {
128+
label := serviceLabel()
129+
fmt.Printf("Homebrew installation detected at %s\n", brewBin)
130+
fmt.Printf("\nThis will register the brew-managed binary as a %s.\n", label)
131+
fmt.Printf("Future upgrades via 'brew upgrade greyproxy' will keep the service current.\n")
132+
133+
if !force {
134+
fmt.Printf("\nProceed? [Y/n] ")
135+
if !askConfirm() {
136+
fmt.Println("You can start the server manually with: greyproxy serve")
137+
fmt.Println("Dashboard: http://localhost:43080")
138+
return
139+
}
140+
}
141+
142+
// Stop and unregister any existing service (may point at ~/.local/bin)
143+
if s, err := newServiceControl(); err == nil {
144+
_ = service.Control(s, "stop")
145+
_ = service.Control(s, "uninstall")
146+
}
147+
148+
s, err := newServiceControlAt(brewBin)
149+
if err != nil {
150+
fmt.Fprintf(os.Stderr, "error: %v\n", err)
151+
os.Exit(1)
152+
}
153+
154+
if err := service.Control(s, "install"); err != nil {
155+
fmt.Fprintf(os.Stderr, "error: registering service: %v\n", err)
156+
os.Exit(1)
157+
}
158+
fmt.Printf("Registered %s\n", label)
159+
160+
if err := service.Control(s, "start"); err != nil {
161+
fmt.Fprintf(os.Stderr, "error: starting service: %v\n", err)
162+
os.Exit(1)
163+
}
164+
fmt.Println("Service started")
165+
fmt.Println("\nDashboard: http://localhost:43080")
166+
}
167+
84168
func handleReinstall(binSrc, binDst string, force bool) {
169+
label := serviceLabel()
85170
fmt.Printf("An existing installation was found at %s\n", binDst)
86171
fmt.Printf("\nReady to update the existing installation. This will:\n")
87172
fmt.Printf(" 1. Stop the running service\n")
88173
fmt.Printf(" 2. Remove the current service registration\n")
89174
fmt.Printf(" 3. Replace the binary with %s\n", binSrc)
90-
fmt.Printf(" 4. Re-register the systemd user service\n")
175+
fmt.Printf(" 4. Re-register the %s\n", label)
91176
fmt.Printf(" 5. Start the service\n")
92177

93178
if !force {
@@ -105,11 +190,11 @@ func handleReinstall(binSrc, binDst string, force bool) {
105190
os.Exit(1)
106191
}
107192

108-
// 1. Stop service (ignore error may already be stopped)
193+
// 1. Stop service (ignore error -- may already be stopped)
109194
_ = service.Control(s, "stop")
110195
fmt.Println("Service stopped")
111196

112-
// 2. Unregister old service (ignore error may not be registered)
197+
// 2. Unregister old service (ignore error -- may not be registered)
113198
_ = service.Control(s, "uninstall")
114199
fmt.Println("Removed old service registration")
115200

@@ -119,6 +204,8 @@ func handleReinstall(binSrc, binDst string, force bool) {
119204
}
120205

121206
func freshInstall(binSrc, binDst string) {
207+
label := serviceLabel()
208+
122209
// Copy binary
123210
if err := copyBinary(binSrc, binDst); err != nil {
124211
fmt.Fprintf(os.Stderr, "error: copying binary: %v\n", err)
@@ -137,7 +224,7 @@ func freshInstall(binSrc, binDst string) {
137224
fmt.Fprintf(os.Stderr, "error: registering service: %v\n", err)
138225
os.Exit(1)
139226
}
140-
fmt.Println("Registered systemd user service")
227+
fmt.Printf("Registered %s\n", label)
141228

142229
// Start service
143230
if err := service.Control(s, "start"); err != nil {
@@ -160,10 +247,11 @@ func askConfirm() bool {
160247
func handleUninstall(args []string) {
161248
force := parseInstallFlags(args)
162249
binDst := installBinPath()
250+
label := serviceLabel()
163251

164252
fmt.Printf("Ready to uninstall greyproxy. This will:\n")
165253
fmt.Printf(" 1. Stop the greyproxy service\n")
166-
fmt.Printf(" 2. Remove the systemd user service\n")
254+
fmt.Printf(" 2. Remove the %s\n", label)
167255
fmt.Printf(" 3. Remove %s\n", binDst)
168256

169257
if !force {
@@ -179,7 +267,7 @@ func handleUninstall(args []string) {
179267
os.Exit(1)
180268
}
181269

182-
// 1. Stop service (ignore error may already be stopped)
270+
// 1. Stop service (ignore error -- may already be stopped)
183271
_ = service.Control(s, "stop")
184272
fmt.Println("Service stopped")
185273

@@ -188,7 +276,7 @@ func handleUninstall(args []string) {
188276
fmt.Fprintf(os.Stderr, "error: removing service: %v\n", err)
189277
os.Exit(1)
190278
}
191-
fmt.Println("Removed systemd user service")
279+
fmt.Printf("Removed %s\n", label)
192280

193281
// 3. Remove binary
194282
if err := os.Remove(binDst); err != nil && !os.IsNotExist(err) {

cmd/greyproxy/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ Usage: greyproxy <command>
159159
160160
Commands:
161161
serve Run the proxy server in foreground
162-
install Install binary and register systemd user service [-f]
162+
install Install binary and register as a background service [-f]
163163
uninstall Stop service, remove registration and binary [-f]
164164
service Manage the OS service (start/stop/restart/status/...)
165165

0 commit comments

Comments
 (0)