package cli import ( "fmt" "os" "runtime" "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 }