Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 13 additions & 36 deletions cmd/devproxy/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
56 changes: 7 additions & 49 deletions cmd/devproxy/cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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 {
Expand Down Expand Up @@ -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")
},
}

Expand All @@ -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) {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions cmd/devproxy/cmd/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
64 changes: 0 additions & 64 deletions internal/service/activation_darwin.go

This file was deleted.

17 changes: 0 additions & 17 deletions internal/service/activation_other.go

This file was deleted.

Loading