diff --git a/README.md b/README.md index 40bc118..d161c12 100644 --- a/README.md +++ b/README.md @@ -116,16 +116,10 @@ ss service reload [--name ] # reload config via mihomo's API, no restart ss service status [--name ] ``` -Linux / macOS (installing the service needs root): -```bash -sudo env SF_CONFIG_DIR=$HOME/basicfiles/cli/ss ss service install -``` +`install`/`uninstall`/`start`/`stop`/`restart` need root/admin privileges: -Windows (run from an elevated/Administrator shell): -```powershell -$env:SF_CONFIG_DIR = "$HOME\basicfiles\cli\ss" -ss.exe service install -``` +- **Linux / macOS**: just run the command directly — `ss` re-execs itself under `sudo` automatically, prompting for your password if needed. +- **Windows**: run from an elevated/Administrator shell — `ss.exe service install`. ## Development diff --git a/internal/cli/elevate.go b/internal/cli/elevate.go new file mode 100644 index 0000000..2896708 --- /dev/null +++ b/internal/cli/elevate.go @@ -0,0 +1,57 @@ +package cli + +import ( + "fmt" + "os" + "os/exec" + "runtime" + + "gitea.epss.net.cn/klesh/ss/internal/storage" +) + +// ensureRoot re-execs the current command under sudo if it isn't already +// running as root. It resolves the config directory *before* elevating and +// passes it through explicitly as SF_CONFIG_DIR, because sudo resets $HOME +// to /root's — without this, the elevated process would silently resolve +// the default config directory under /root instead of the invoking user's +// home. +// +// Windows is handled separately: UAC elevation launches a new process that +// can't transparently inherit the current console, so internal/service's +// runAsAdmin instead prints manual elevation instructions rather than +// auto-elevating. +func ensureRoot() error { + if runtime.GOOS == "windows" { + return nil + } + if os.Geteuid() == 0 { + return nil + } + + s, err := storage.New() + if err != nil { + return fmt.Errorf("failed to resolve config directory: %w", err) + } + + self, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to determine executable path: %w", err) + } + + sudoPath, err := exec.LookPath("sudo") + if err != nil { + return fmt.Errorf("this command requires root privileges, but sudo was not found on PATH: %w", err) + } + + args := append([]string{"env", "SF_CONFIG_DIR=" + s.ConfigDir, self}, os.Args[1:]...) + cmd := exec.Command(sudoPath, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + runErr := cmd.Run() + if cmd.ProcessState != nil { + os.Exit(cmd.ProcessState.ExitCode()) + } + return fmt.Errorf("failed to re-exec under sudo: %w", runErr) +} diff --git a/internal/cli/service.go b/internal/cli/service.go index ed0b237..7594895 100644 --- a/internal/cli/service.go +++ b/internal/cli/service.go @@ -40,6 +40,9 @@ func newServiceInstallCmd() *cobra.Command { Short: "Install mihomo as a system service", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + if err := ensureRoot(); err != nil { + return err + } m, err := newManagers("", "", "") if err != nil { return err @@ -62,6 +65,9 @@ func newServiceUninstallCmd() *cobra.Command { Short: "Uninstall mihomo system service", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + if err := ensureRoot(); err != nil { + return err + } m, err := newManagers("", "", "") if err != nil { return err @@ -83,6 +89,9 @@ func newServiceStartCmd() *cobra.Command { Short: "Start mihomo system service", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + if err := ensureRoot(); err != nil { + return err + } m, err := newManagers("", "", "") if err != nil { return err @@ -104,6 +113,9 @@ func newServiceStopCmd() *cobra.Command { Short: "Stop mihomo system service", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + if err := ensureRoot(); err != nil { + return err + } m, err := newManagers("", "", "") if err != nil { return err @@ -125,6 +137,9 @@ func newServiceRestartCmd() *cobra.Command { Short: "Restart mihomo system service", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + if err := ensureRoot(); err != nil { + return err + } m, err := newManagers("", "", "") if err != nil { return err