diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a774c5..5d2d14a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.2.3 2026-01-30 + +### Removed + +- **Service manager integration removed** - The `internal/service` package and all launchd/systemd integration code has been removed + - Removed `--service` flag from `devproxy setup` + - Removed socket activation support + - Removed automatic service installation/uninstallation + - Removed `go-launchd` dependency + +### Changed + +- `devproxy status` no longer shows socket activation state +- `devproxy setup` no longer offers to install system services +- `devproxy uninstall` no longer removes system services + +### Migration + +If you previously installed devproxy as a system service, you must manually remove it: + +**macOS:** +```bash +sudo launchctl unload /Library/LaunchDaemons/com.devproxy.daemon.plist +sudo rm /Library/LaunchDaemons/com.devproxy.daemon.plist +``` + +**Linux:** +```bash +sudo systemctl stop devproxy +sudo systemctl disable devproxy +sudo rm /etc/systemd/system/devproxy.service +sudo systemctl daemon-reload +``` + +After removal, use `devproxy start` to run as a daemon or `devproxy run` for foreground mode. + ## 0.2.2 2026-01-30 ### Changed @@ -19,6 +55,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 sudo devproxy setup --service ``` +- `devproxy status` now correctly shows socket activation state + - Shows "ready (socket activation)" when service is loaded but idle + - Displays which ports are owned by launchd + - Shows routes from last run when socket-activated but not yet running + ## 0.2.1 2026-01-19 ### Changed diff --git a/README.md b/README.md index b40a209..b1396c5 100644 --- a/README.md +++ b/README.md @@ -55,10 +55,6 @@ brew install munichmade/tap/devproxy curl -L https://github.com/munichmade/devproxy/releases/latest/download/devproxy-linux-amd64 -o devproxy chmod +x devproxy sudo mv devproxy /usr/local/bin/ - -# Install systemd service -sudo cp init/devproxy.service /etc/systemd/system/ -sudo systemctl daemon-reload ``` ### Build from Source diff --git a/cmd/devproxy/cmd/run.go b/cmd/devproxy/cmd/run.go index 218dead..00f2db2 100644 --- a/cmd/devproxy/cmd/run.go +++ b/cmd/devproxy/cmd/run.go @@ -21,7 +21,6 @@ import ( "github.com/munichmade/devproxy/internal/paths" "github.com/munichmade/devproxy/internal/privilege" "github.com/munichmade/devproxy/internal/proxy" - "github.com/munichmade/devproxy/internal/service" ) // chownRecursive changes ownership of a directory and all its contents @@ -39,8 +38,8 @@ func chownRecursive(path string, uid, gid int) error { var runCmd = &cobra.Command{ Use: "run", - Short: "Run daemon in foreground (for systemd/launchd)", - Long: `Run the devproxy daemon in the foreground. Used by systemd/launchd service managers.`, + Short: "Run daemon in foreground", + Long: `Run the devproxy daemon in the foreground. Useful for debugging or running under a process supervisor.`, Hidden: true, Run: func(cmd *cobra.Command, args []string) { if err := runDaemon(); err != nil { @@ -73,43 +72,22 @@ func runDaemon() error { } // ========================================================================= - // PRIVILEGED SECTION - Try socket activation, fall back to direct binding + // PRIVILEGED SECTION - Bind to privileged ports while running as root // ========================================================================= httpCfg, _ := cfg.GetEntrypoint("http") httpsCfg, _ := cfg.GetEntrypoint("https") - // Try launchd socket activation first (macOS service) - // When running under launchd, sockets are pre-bound by the system - httpListener, err := service.ActivatedListener("HTTPListener") + // Bind HTTP port + httpListener, err := net.Listen("tcp", httpCfg.Listen) if err != nil { - logging.Debug("launchd HTTP socket activation failed", "error", err) - } - httpsListener, err := service.ActivatedListener("HTTPSListener") - if err != nil { - logging.Debug("launchd HTTPS socket activation failed", "error", err) - } - - // Track if we successfully used socket activation - usedSocketActivation := httpListener != nil && httpsListener != nil - - if usedSocketActivation { - fmt.Fprintf(os.Stderr, "using launchd socket activation for HTTP/HTTPS\n") - } - - // Fall back to direct binding if socket activation not available - if httpListener == nil { - httpListener, err = net.Listen("tcp", httpCfg.Listen) - if err != nil { - return fmt.Errorf("failed to bind HTTP port %s: %w", httpCfg.Listen, err) - } + return fmt.Errorf("failed to bind HTTP port %s: %w", httpCfg.Listen, err) } - if httpsListener == nil { - httpsListener, err = net.Listen("tcp", httpsCfg.Listen) - if err != nil { - httpListener.Close() - return fmt.Errorf("failed to bind HTTPS port %s: %w", httpsCfg.Listen, err) - } + // Bind HTTPS port + httpsListener, err := net.Listen("tcp", httpsCfg.Listen) + if err != nil { + httpListener.Close() + return fmt.Errorf("failed to bind HTTPS port %s: %w", httpsCfg.Listen, err) } // Bind DNS port if enabled @@ -146,10 +124,9 @@ func runDaemon() error { } // ========================================================================= - // DROP PRIVILEGES - Only needed when NOT using socket activation - // When using launchd socket activation, we're already running unprivileged + // DROP PRIVILEGES - Run as original user after binding privileged ports // ========================================================================= - if !usedSocketActivation && originalUser != nil { + if originalUser != nil { // Before dropping privileges, ensure data directories are owned by the original user // This is needed because directories may have been created by root dataDir := paths.DataDir() diff --git a/cmd/devproxy/cmd/setup.go b/cmd/devproxy/cmd/setup.go index c14b906..9b6ffbc 100644 --- a/cmd/devproxy/cmd/setup.go +++ b/cmd/devproxy/cmd/setup.go @@ -10,7 +10,6 @@ import ( "github.com/munichmade/devproxy/internal/config" "github.com/munichmade/devproxy/internal/privilege" "github.com/munichmade/devproxy/internal/resolver" - "github.com/munichmade/devproxy/internal/service" ) var setupCmd = &cobra.Command{ @@ -21,12 +20,10 @@ var setupCmd = &cobra.Command{ 1. Generating a local Certificate Authority (CA) if not present 2. Installing the CA into the system trust store 3. Configuring DNS resolver for *.localhost domains - 4. Optionally installing devproxy as a system service (--service flag) Administrator privileges are required to install the CA and configure DNS.`, Run: func(cmd *cobra.Command, args []string) { domains, _ := cmd.Flags().GetStringSlice("domain") - installService, _ := cmd.Flags().GetBool("service") // Elevate to root if needed if err := privilege.RequireRoot("installing CA certificate and configuring DNS resolver"); err != nil { @@ -88,35 +85,10 @@ Administrator privileges are required to install the CA and configure DNS.`, fmt.Printf(" DNS resolver configured for: %v (port %d)\n", domains, dnsPort) } - // Step 4: Install as system service (optional) - if installService { - fmt.Print("4. Installing system service... ") - if service.IsInstalled() { - fmt.Println("already installed") - } else { - if err := service.Install(service.Config{}); err != nil { - fmt.Fprintf(os.Stderr, "failed: %v\n", err) - os.Exit(1) - } - fmt.Printf(" Service installed (%s)\n", service.ServiceName()) - - fmt.Print(" Starting service... ") - if err := service.Start(); err != nil { - fmt.Fprintf(os.Stderr, "failed: %v\n", err) - os.Exit(1) - } - fmt.Println("started") - } - } - fmt.Println() fmt.Println("Setup complete! devproxy is ready to use.") fmt.Println() - if installService { - fmt.Println("The service is running and will start automatically on boot.") - } else { - fmt.Println("Start the daemon with: devproxy start") - } + fmt.Println("Start the daemon with: devproxy start") }, } @@ -125,9 +97,8 @@ var uninstallCmd = &cobra.Command{ Short: "Remove devproxy system configuration", Long: `Uninstall removes devproxy system configuration: - 1. Uninstalls system service (if installed) - 2. Removes CA from system trust store - 3. Removes DNS resolver configuration + 1. Removes CA from system trust store + 2. Removes DNS resolver configuration Administrator privileges are required.`, Run: func(cmd *cobra.Command, args []string) { @@ -142,20 +113,8 @@ Administrator privileges are required.`, fmt.Println("Removing devproxy configuration...") fmt.Println() - // Step 1: Uninstall system service - fmt.Print("1. Removing system service... ") - if !service.IsInstalled() { - fmt.Println("not installed") - } else { - if err := service.Uninstall(); err != nil { - fmt.Fprintf(os.Stderr, "failed: %v\n", err) - } else { - fmt.Println("done") - } - } - - // Step 2: Remove CA trust - fmt.Print("2. Removing CA from trust store... ") + // Step 1: Remove CA trust + fmt.Print("1. Removing CA from trust store... ") if !ca.IsTrusted() { fmt.Println("not installed") } else { @@ -166,8 +125,8 @@ Administrator privileges are required.`, } } - // Step 3: Remove DNS resolver - fmt.Print("3. Removing DNS resolver... ") + // Step 2: Remove DNS resolver + fmt.Print("2. Removing DNS resolver... ") if !resolver.IsConfigured(domains) { fmt.Println("not configured") } else { @@ -209,7 +168,6 @@ func extractPort(addr string, defaultPort int) int { func init() { setupCmd.Flags().StringSliceP("domain", "d", []string{"localhost"}, "Domains to configure") - setupCmd.Flags().BoolP("service", "s", false, "Install as system service (launchd on macOS, systemd on Linux)") uninstallCmd.Flags().StringSliceP("domain", "d", []string{"localhost"}, "Domains to remove") rootCmd.AddCommand(setupCmd) diff --git a/cmd/devproxy/cmd/status.go b/cmd/devproxy/cmd/status.go index 9c9cdb0..d939ee2 100644 --- a/cmd/devproxy/cmd/status.go +++ b/cmd/devproxy/cmd/status.go @@ -73,12 +73,13 @@ func getStatus() Status { d := daemon.New() status := Status{ - Running: d.IsRunning(), Entrypoints: []Entrypoint{}, Projects: make(map[string]*ProjectRoutes), } - if status.Running { + // Check if daemon is running via PID file + if d.IsRunning() { + status.Running = true pid, _ := d.GetPID() status.PID = pid } diff --git a/go.mod b/go.mod index 21c5c26..3a46b70 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( require ( github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/bored-engineer/go-launchd v0.0.0-20241220214420-b514778f4f80 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect diff --git a/go.sum b/go.sum index b3743c0..b2689ef 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/bored-engineer/go-launchd v0.0.0-20241220214420-b514778f4f80 h1:O7c0CDkfGaIa7eNktOIBoPpuaq0zazOqB3iTghXIySw= -github.com/bored-engineer/go-launchd v0.0.0-20241220214420-b514778f4f80/go.mod h1:fuBezjGxBjQFkJeCIN1eTo9c27q2WxjHK7tRlfBkyQk= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= diff --git a/internal/service/activation_darwin.go b/internal/service/activation_darwin.go deleted file mode 100644 index 0046b3b..0000000 --- a/internal/service/activation_darwin.go +++ /dev/null @@ -1,64 +0,0 @@ -//go:build darwin - -package service - -import ( - "net" - "sync" - "syscall" - - launchd "github.com/bored-engineer/go-launchd" -) - -var ( - activatedListeners = make(map[string]net.Listener) - activationMu sync.Mutex - activationChecked = make(map[string]bool) -) - -// ActivatedListener returns a listener from launchd socket activation. -// Returns nil, nil if not running under launchd or socket not available. -// Results are cached since launchd only allows one activation per socket name. -func ActivatedListener(name string) (net.Listener, error) { - activationMu.Lock() - defer activationMu.Unlock() - - // Return cached listener if already activated - if l, ok := activatedListeners[name]; ok { - return l, nil - } - - // If we already checked this socket and it wasn't available, don't retry - if activationChecked[name] { - return nil, nil - } - activationChecked[name] = true - - // Try to activate from launchd - listener, err := launchd.Activate(name) - if err != nil { - // ESRCH = not running under launchd - // ENOENT = socket name not found in plist - // These are expected when not running as a launchd service - if err == syscall.ESRCH || err == syscall.ENOENT { - return nil, nil - } - // EALREADY = already activated (shouldn't happen due to caching, but handle it) - if err == syscall.EALREADY { - return nil, nil - } - // Unexpected error - return nil, err - } - - activatedListeners[name] = listener - return listener, nil -} - -// IsSocketActivated returns true if the process was started via launchd -// socket activation and has activated listeners available. -func IsSocketActivated() bool { - activationMu.Lock() - defer activationMu.Unlock() - return len(activatedListeners) > 0 -} diff --git a/internal/service/activation_other.go b/internal/service/activation_other.go deleted file mode 100644 index 6ff01e2..0000000 --- a/internal/service/activation_other.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build !darwin - -package service - -import "net" - -// ActivatedListener returns nil on non-Darwin platforms. -// Socket activation via launchd is only supported on macOS. -// Linux systemd socket activation can be added in the future. -func ActivatedListener(name string) (net.Listener, error) { - return nil, nil -} - -// IsSocketActivated returns false on non-Darwin platforms. -func IsSocketActivated() bool { - return false -} diff --git a/internal/service/activation_other_test.go b/internal/service/activation_other_test.go deleted file mode 100644 index faac48f..0000000 --- a/internal/service/activation_other_test.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build !darwin - -package service - -import ( - "testing" -) - -func TestActivatedListener_NonDarwin(t *testing.T) { - // On non-Darwin platforms, ActivatedListener should always return nil - listener, err := ActivatedListener("HTTPListener") - if err != nil { - t.Errorf("ActivatedListener should not return error on non-Darwin: %v", err) - } - if listener != nil { - listener.Close() - t.Error("ActivatedListener should return nil on non-Darwin platforms") - } -} - -func TestIsSocketActivated_NonDarwin(t *testing.T) { - if IsSocketActivated() { - t.Error("IsSocketActivated should always return false on non-Darwin platforms") - } -} diff --git a/internal/service/service.go b/internal/service/service.go deleted file mode 100644 index aa253e4..0000000 --- a/internal/service/service.go +++ /dev/null @@ -1,56 +0,0 @@ -// Package service provides system service installation and management for devproxy. -package service - -import ( - "fmt" - "os" - "path/filepath" -) - -// Config holds service installation configuration. -type Config struct { - // BinaryPath is the path to the devproxy binary. - // If empty, the current executable path is used. - BinaryPath string -} - -// IsInstalled checks if devproxy is installed as a system service. -func IsInstalled() bool { - return isInstalled() -} - -// Install installs devproxy as a system service. -func Install(cfg Config) error { - if cfg.BinaryPath == "" { - exe, err := os.Executable() - if err != nil { - return fmt.Errorf("failed to get executable path: %w", err) - } - cfg.BinaryPath, err = filepath.EvalSymlinks(exe) - if err != nil { - return fmt.Errorf("failed to resolve executable path: %w", err) - } - } - - return install(cfg) -} - -// Uninstall removes devproxy from system services. -func Uninstall() error { - return uninstall() -} - -// Start starts the system service. -func Start() error { - return start() -} - -// Stop stops the system service. -func Stop() error { - return stop() -} - -// ServiceName returns the name of the service for the current platform. -func ServiceName() string { - return serviceName() -} diff --git a/internal/service/service_darwin.go b/internal/service/service_darwin.go deleted file mode 100644 index ab2f3c0..0000000 --- a/internal/service/service_darwin.go +++ /dev/null @@ -1,113 +0,0 @@ -//go:build darwin - -package service - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" -) - -const ( - plistName = "com.devproxy.daemon.plist" - plistDir = "/Library/LaunchDaemons" -) - -func serviceName() string { - return "com.devproxy.daemon" -} - -func plistPath() string { - return filepath.Join(plistDir, plistName) -} - -func isInstalled() bool { - _, err := os.Stat(plistPath()) - return err == nil -} - -func install(cfg Config) error { - plist := generatePlist(cfg.BinaryPath) - - // Ensure the directory exists - if err := os.MkdirAll(plistDir, 0o755); err != nil { - return fmt.Errorf("failed to create %s: %w", plistDir, err) - } - - // Write the plist file - if err := os.WriteFile(plistPath(), []byte(plist), 0o644); err != nil { - return fmt.Errorf("failed to write plist: %w", err) - } - - return nil -} - -func uninstall() error { - // Stop the service first if running - _ = stop() - - // Remove the plist file - if err := os.Remove(plistPath()); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to remove plist: %w", err) - } - - return nil -} - -func start() error { - cmd := exec.Command("launchctl", "load", plistPath()) - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to load service: %w: %s", err, strings.TrimSpace(string(output))) - } - return nil -} - -func stop() error { - cmd := exec.Command("launchctl", "unload", plistPath()) - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to unload service: %w: %s", err, strings.TrimSpace(string(output))) - } - return nil -} - -func generatePlist(binaryPath string) string { - return fmt.Sprintf(` - - - - Label - com.devproxy.daemon - ProgramArguments - - %s - run - - RunAtLoad - - Sockets - - HTTPListener - - SockServiceName - 80 - SockType - stream - - HTTPSListener - - SockServiceName - 443 - SockType - stream - - - StandardOutPath - /var/log/devproxy.log - StandardErrorPath - /var/log/devproxy.log - - -`, binaryPath) -} diff --git a/internal/service/service_darwin_test.go b/internal/service/service_darwin_test.go deleted file mode 100644 index 2061c5e..0000000 --- a/internal/service/service_darwin_test.go +++ /dev/null @@ -1,126 +0,0 @@ -//go:build darwin - -package service - -import ( - "net" - "strings" - "testing" -) - -func TestGeneratePlist(t *testing.T) { - binaryPath := "/usr/local/bin/devproxy" - plist := generatePlist(binaryPath) - - t.Run("contains binary path", func(t *testing.T) { - if !strings.Contains(plist, binaryPath) { - t.Errorf("plist should contain binary path %s", binaryPath) - } - }) - - t.Run("contains service label", func(t *testing.T) { - if !strings.Contains(plist, "com.devproxy.daemon") { - t.Error("plist should contain service label com.devproxy.daemon") - } - }) - - t.Run("contains run argument", func(t *testing.T) { - if !strings.Contains(plist, "run") { - t.Error("plist should contain 'run' argument") - } - }) - - t.Run("contains Sockets dictionary", func(t *testing.T) { - if !strings.Contains(plist, "Sockets") { - t.Error("plist should contain Sockets key for socket activation") - } - }) - - t.Run("contains HTTPListener socket", func(t *testing.T) { - if !strings.Contains(plist, "HTTPListener") { - t.Error("plist should contain HTTPListener socket") - } - if !strings.Contains(plist, "80") { - t.Error("plist should configure port 80 for HTTPListener") - } - }) - - t.Run("contains HTTPSListener socket", func(t *testing.T) { - if !strings.Contains(plist, "HTTPSListener") { - t.Error("plist should contain HTTPSListener socket") - } - if !strings.Contains(plist, "443") { - t.Error("plist should configure port 443 for HTTPSListener") - } - }) - - t.Run("does not contain KeepAlive", func(t *testing.T) { - if strings.Contains(plist, "KeepAlive") { - t.Error("plist should not contain KeepAlive (using socket activation instead)") - } - }) - - t.Run("contains RunAtLoad", func(t *testing.T) { - if !strings.Contains(plist, "RunAtLoad") { - t.Error("plist should contain RunAtLoad") - } - }) - - t.Run("contains log paths", func(t *testing.T) { - if !strings.Contains(plist, "/var/log/devproxy.log") { - t.Error("plist should configure log path") - } - }) -} - -func TestActivatedListener_NotUnderLaunchd(t *testing.T) { - // When not running under launchd, ActivatedListener should return nil - // without error (graceful fallback) - listener, err := ActivatedListener("HTTPListener") - if err != nil { - t.Errorf("ActivatedListener should not return error when not under launchd: %v", err) - } - if listener != nil { - listener.Close() - t.Error("ActivatedListener should return nil when not running under launchd") - } -} - -func TestActivatedListener_Caching(t *testing.T) { - // Reset state for test - activationMu.Lock() - activatedListeners = make(map[string]net.Listener) - activationChecked = make(map[string]bool) - activationMu.Unlock() - - // First call should check launchd - listener1, _ := ActivatedListener("TestSocket") - - // Second call should use cached result (nil in this case since not under launchd) - listener2, _ := ActivatedListener("TestSocket") - - // Both should be nil (not under launchd) - if listener1 != nil || listener2 != nil { - t.Error("expected nil listeners when not under launchd") - } - - // Verify cache was used - activationMu.Lock() - checked := activationChecked["TestSocket"] - activationMu.Unlock() - - if !checked { - t.Error("expected socket to be marked as checked after first call") - } -} - -func TestIsSocketActivated(t *testing.T) { - // Reset state - activationMu.Lock() - activatedListeners = make(map[string]net.Listener) - activationMu.Unlock() - - if IsSocketActivated() { - t.Error("IsSocketActivated should return false when no listeners are activated") - } -} diff --git a/internal/service/service_linux.go b/internal/service/service_linux.go deleted file mode 100644 index d44640d..0000000 --- a/internal/service/service_linux.go +++ /dev/null @@ -1,112 +0,0 @@ -//go:build linux - -package service - -import ( - "fmt" - "os" - "os/exec" - "strings" -) - -const ( - serviceName_ = "devproxy" - serviceDir = "/etc/systemd/system" - serviceFileName = "devproxy.service" -) - -func serviceName() string { - return serviceName_ -} - -func servicePath() string { - return serviceDir + "/" + serviceFileName -} - -func isInstalled() bool { - _, err := os.Stat(servicePath()) - return err == nil -} - -func install(cfg Config) error { - unit := generateUnit(cfg.BinaryPath) - - // Ensure the directory exists - if err := os.MkdirAll(serviceDir, 0755); err != nil { - return fmt.Errorf("failed to create %s: %w", serviceDir, err) - } - - // Write the service file - if err := os.WriteFile(servicePath(), []byte(unit), 0644); err != nil { - return fmt.Errorf("failed to write service file: %w", err) - } - - // Reload systemd daemon - cmd := exec.Command("systemctl", "daemon-reload") - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to reload systemd: %w: %s", err, strings.TrimSpace(string(output))) - } - - // Enable the service - cmd = exec.Command("systemctl", "enable", serviceName_) - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to enable service: %w: %s", err, strings.TrimSpace(string(output))) - } - - return nil -} - -func uninstall() error { - // Stop the service first - _ = stop() - - // Disable the service - cmd := exec.Command("systemctl", "disable", serviceName_) - _ = cmd.Run() // Ignore errors if not enabled - - // Remove the service file - if err := os.Remove(servicePath()); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to remove service file: %w", err) - } - - // Reload systemd daemon - cmd = exec.Command("systemctl", "daemon-reload") - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to reload systemd: %w: %s", err, strings.TrimSpace(string(output))) - } - - return nil -} - -func start() error { - cmd := exec.Command("systemctl", "start", serviceName_) - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to start service: %w: %s", err, strings.TrimSpace(string(output))) - } - return nil -} - -func stop() error { - cmd := exec.Command("systemctl", "stop", serviceName_) - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to stop service: %w: %s", err, strings.TrimSpace(string(output))) - } - return nil -} - -func generateUnit(binaryPath string) string { - return fmt.Sprintf(`[Unit] -Description=Development Proxy -After=network.target docker.service -Wants=docker.service - -[Service] -Type=simple -ExecStart=%s run -Restart=always -RestartSec=5 - -[Install] -WantedBy=multi-user.target -`, binaryPath) -} diff --git a/scripts/cleanup-service-integration.sh b/scripts/cleanup-service-integration.sh new file mode 100755 index 0000000..4617919 --- /dev/null +++ b/scripts/cleanup-service-integration.sh @@ -0,0 +1,113 @@ +#!/bin/bash +# +# cleanup-service-integration.sh +# +# Removes devproxy system service files that were installed by previous versions. +# Run this script if you previously used `devproxy setup --service` or manually +# installed devproxy as a launchd/systemd service. +# +# Usage: sudo ./cleanup-service-integration.sh +# + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if running as root +if [[ $EUID -ne 0 ]]; then + error "This script must be run as root (use sudo)" + exit 1 +fi + +echo "Devproxy Service Cleanup Script" +echo "================================" +echo "" + +# Detect OS +case "$(uname -s)" in + Darwin) + info "Detected macOS" + + PLIST_PATH="/Library/LaunchDaemons/com.devproxy.daemon.plist" + + if [[ -f "$PLIST_PATH" ]]; then + info "Found launchd plist at $PLIST_PATH" + + # Unload the service if loaded + if launchctl list | grep -q "com.devproxy.daemon"; then + info "Unloading service..." + launchctl unload "$PLIST_PATH" 2>/dev/null || true + fi + + # Remove the plist file + info "Removing plist file..." + rm -f "$PLIST_PATH" + + info "macOS service removed successfully" + else + info "No launchd plist found at $PLIST_PATH" + fi + ;; + + Linux) + info "Detected Linux" + + SYSTEMD_PATH="/etc/systemd/system/devproxy.service" + + if [[ -f "$SYSTEMD_PATH" ]]; then + info "Found systemd unit at $SYSTEMD_PATH" + + # Stop the service if running + if systemctl is-active --quiet devproxy 2>/dev/null; then + info "Stopping service..." + systemctl stop devproxy + fi + + # Disable the service + if systemctl is-enabled --quiet devproxy 2>/dev/null; then + info "Disabling service..." + systemctl disable devproxy + fi + + # Remove the unit file + info "Removing unit file..." + rm -f "$SYSTEMD_PATH" + + # Reload systemd + info "Reloading systemd daemon..." + systemctl daemon-reload + + info "Linux service removed successfully" + else + info "No systemd unit found at $SYSTEMD_PATH" + fi + ;; + + *) + error "Unsupported operating system: $(uname -s)" + exit 1 + ;; +esac + +echo "" +info "Cleanup complete!" +echo "" +echo "You can now run devproxy using:" +echo " devproxy start # Run as background daemon" +echo " devproxy run # Run in foreground"