Files
ctrld/cmd/cli/commands_service.go
2026-03-10 17:13:33 +07:00

308 lines
9.8 KiB
Go

package cli
import (
"fmt"
"os"
"runtime"
"strings"
"github.com/kardianos/service"
"github.com/spf13/cobra"
)
// filterEmptyStrings removes empty strings from a slice
// This is used to clean up command line arguments and configuration values
func filterEmptyStrings(slice []string) []string {
var result []string
for _, s := range slice {
if s != "" {
result = append(result, s)
}
}
return result
}
// ServiceCommand handles service-related operations
// This encapsulates all service management functionality for the CLI
type ServiceCommand struct {
serviceManager *ServiceManager
}
// initializeServiceManager creates a service manager with default configuration
// This sets up the basic service infrastructure needed for all service operations
func (sc *ServiceCommand) initializeServiceManager() (service.Service, *prog, error) {
svcConfig := sc.createServiceConfig()
return sc.initializeServiceManagerWithServiceConfig(svcConfig)
}
// initializeServiceManagerWithServiceConfig creates a service manager with the given configuration
// This allows for custom service configuration while maintaining the same initialization pattern
func (sc *ServiceCommand) initializeServiceManagerWithServiceConfig(svcConfig *service.Config) (service.Service, *prog, error) {
p := &prog{}
s, err := sc.newService(p, svcConfig)
if err != nil {
return nil, nil, fmt.Errorf("failed to create service: %w", err)
}
sc.serviceManager = &ServiceManager{prog: p, svc: s}
return s, p, nil
}
// newService creates a new service instance using the provided program and configuration.
// This abstracts the service creation process for different operating systems
func (sc *ServiceCommand) newService(p *prog, svcConfig *service.Config) (service.Service, error) {
s, err := newService(p, svcConfig)
if err != nil {
return nil, fmt.Errorf("failed to create service: %w", err)
}
return s, nil
}
// NewServiceCommand creates a new service command handler
// This provides a clean factory method for creating service command instances
func NewServiceCommand() *ServiceCommand {
return &ServiceCommand{}
}
// createServiceConfig creates a properly initialized service configuration
// This ensures consistent service naming and description across all platforms
func (sc *ServiceCommand) createServiceConfig() *service.Config {
return &service.Config{
Name: ctrldServiceName,
DisplayName: "Control-D Helper Service",
Description: "A highly configurable, multi-protocol DNS forwarding proxy",
Option: service.KeyValue{},
}
}
// InitServiceCmd creates the service command with proper logic and aliases
// This sets up all service-related subcommands with appropriate permissions and flags
func InitServiceCmd(rootCmd *cobra.Command) *cobra.Command {
// Create service command handlers
sc := NewServiceCommand()
startCmd, startCmdAlias := createStartCommands(sc)
rootCmd.AddCommand(startCmdAlias)
// Stop command
stopCmd := &cobra.Command{
Use: "stop",
Short: "Stop the ctrld service",
Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
checkHasElevatedPrivilege()
},
RunE: sc.Stop,
}
stopCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, "auto" means the default interface gateway`)
stopCmd.Flags().Int64VarP(&deactivationPin, "pin", "", defaultDeactivationPin, `Pin code for stopping ctrld`)
_ = stopCmd.Flags().MarkHidden("pin")
// Restart command
restartCmd := &cobra.Command{
Use: "restart",
Short: "Restart the ctrld service",
Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
checkHasElevatedPrivilege()
},
RunE: sc.Restart,
}
// Status command
statusCmd := &cobra.Command{
Use: "status",
Short: "Show status of the ctrld service",
Args: cobra.NoArgs,
RunE: sc.Status,
}
if runtime.GOOS == "darwin" {
// On darwin, running status command without privileges may return wrong information.
statusCmd.PreRun = func(cmd *cobra.Command, args []string) {
checkHasElevatedPrivilege()
}
}
// Reload command
reloadCmd := &cobra.Command{
Use: "reload",
Short: "Reload the ctrld service",
Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
checkHasElevatedPrivilege()
},
RunE: sc.Reload,
}
// Uninstall command
uninstallCmd := &cobra.Command{
Use: "uninstall",
Short: "Stop and uninstall the ctrld service",
Long: `Stop and uninstall the ctrld service.
NOTE: Uninstalling will set DNS to values provided by DHCP.`,
Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
checkHasElevatedPrivilege()
},
RunE: sc.Uninstall,
}
uninstallCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, "auto" means the default interface gateway`)
uninstallCmd.Flags().Int64VarP(&deactivationPin, "pin", "", defaultDeactivationPin, `Pin code for stopping ctrld`)
_ = uninstallCmd.Flags().MarkHidden("pin")
uninstallCmd.Flags().BoolVarP(&cleanup, "cleanup", "", false, `Removing ctrld binary and config files`)
// Interfaces command - use the existing InitInterfacesCmd function
interfacesCmd := InitInterfacesCmd(rootCmd)
stopCmdAlias := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) {
checkHasElevatedPrivilege()
},
Use: "stop",
Short: "Quick stop service and remove DNS from interface",
RunE: func(cmd *cobra.Command, args []string) error {
if !cmd.Flags().Changed("iface") {
os.Args = append(os.Args, "--iface="+ifaceStartStop)
}
iface = ifaceStartStop
return stopCmd.RunE(cmd, args)
},
}
stopCmdAlias.Flags().StringVarP(&ifaceStartStop, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`)
stopCmdAlias.Flags().AddFlagSet(stopCmd.Flags())
rootCmd.AddCommand(stopCmdAlias)
// Create aliases for other service commands
restartCmdAlias := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) {
checkHasElevatedPrivilege()
},
Use: "restart",
Short: "Restart the ctrld service",
RunE: func(cmd *cobra.Command, args []string) error {
return restartCmd.RunE(cmd, args)
},
}
rootCmd.AddCommand(restartCmdAlias)
reloadCmdAlias := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) {
checkHasElevatedPrivilege()
},
Use: "reload",
Short: "Reload the ctrld service",
RunE: func(cmd *cobra.Command, args []string) error {
return reloadCmd.RunE(cmd, args)
},
}
rootCmd.AddCommand(reloadCmdAlias)
statusCmdAlias := &cobra.Command{
Use: "status",
Short: "Show status of the ctrld service",
Args: cobra.NoArgs,
RunE: statusCmd.RunE,
}
rootCmd.AddCommand(statusCmdAlias)
uninstallCmdAlias := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) {
checkHasElevatedPrivilege()
},
Use: "uninstall",
Short: "Stop and uninstall the ctrld service",
Long: `Stop and uninstall the ctrld service.
NOTE: Uninstalling will set DNS to values provided by DHCP.`,
RunE: func(cmd *cobra.Command, args []string) error {
if !cmd.Flags().Changed("iface") {
os.Args = append(os.Args, "--iface="+ifaceStartStop)
}
iface = ifaceStartStop
return uninstallCmd.RunE(cmd, args)
},
}
uninstallCmdAlias.Flags().StringVarP(&ifaceStartStop, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`)
uninstallCmdAlias.Flags().AddFlagSet(uninstallCmd.Flags())
rootCmd.AddCommand(uninstallCmdAlias)
// Create service command
serviceCmd := &cobra.Command{
Use: "service",
Short: "Manage ctrld service",
Args: cobra.OnlyValidArgs,
}
serviceCmd.ValidArgs = make([]string, 7)
serviceCmd.ValidArgs[0] = startCmd.Use
serviceCmd.ValidArgs[1] = stopCmd.Use
serviceCmd.ValidArgs[2] = restartCmd.Use
serviceCmd.ValidArgs[3] = reloadCmd.Use
serviceCmd.ValidArgs[4] = statusCmd.Use
serviceCmd.ValidArgs[5] = uninstallCmd.Use
serviceCmd.ValidArgs[6] = interfacesCmd.Use
serviceCmd.AddCommand(startCmd)
serviceCmd.AddCommand(stopCmd)
serviceCmd.AddCommand(restartCmd)
serviceCmd.AddCommand(reloadCmd)
serviceCmd.AddCommand(statusCmd)
serviceCmd.AddCommand(uninstallCmd)
serviceCmd.AddCommand(interfacesCmd)
rootCmd.AddCommand(serviceCmd)
return serviceCmd
}
// validInterceptMode reports whether the given value is a recognized --intercept-mode.
// This is the single source of truth for mode validation — used by the early start
// command check, the runtime validation in prog.go, and onlyInterceptFlags below.
// Add new modes here to have them recognized everywhere.
func validInterceptMode(mode string) bool {
switch mode {
case "off", "dns", "hard":
return true
}
return false
}
// onlyInterceptFlags reports whether args contain only intercept mode
// flags (--intercept-mode <value>) and flags that are auto-added by the
// start command alias (--iface). This is used to detect "ctrld start --intercept-mode dns"
// (or "off" to disable) on an existing installation, where the intent is to modify the
// intercept flag on the existing service without replacing other arguments.
//
// Note: the startCmdAlias appends "--iface=auto" to os.Args when --iface isn't
// explicitly provided, so we must allow it here.
func onlyInterceptFlags(args []string) bool {
hasIntercept := false
for i := 0; i < len(args); i++ {
arg := args[i]
switch {
case arg == "--intercept-mode":
// Next arg must be a valid mode value.
if i+1 < len(args) && validInterceptMode(args[i+1]) {
hasIntercept = true
i++ // skip the value
} else {
return false
}
case strings.HasPrefix(arg, "--intercept-mode="):
val := strings.TrimPrefix(arg, "--intercept-mode=")
if validInterceptMode(val) {
hasIntercept = true
} else {
return false
}
case arg == "--iface=auto" || arg == "--iface" || arg == "auto":
// Auto-added by startCmdAlias or its value; safe to ignore.
continue
default:
return false
}
}
return hasIntercept
}