|
| 1 | +package cli |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "fmt" |
| 6 | + "os" |
| 7 | + "os/exec" |
| 8 | + "path/filepath" |
| 9 | + "runtime" |
| 10 | + "strings" |
| 11 | + |
| 12 | + "github.com/KafClaw/KafClaw/internal/config" |
| 13 | + "github.com/KafClaw/KafClaw/internal/onboarding" |
| 14 | + "github.com/spf13/cobra" |
| 15 | +) |
| 16 | + |
| 17 | +var daemonCmd = &cobra.Command{ |
| 18 | + Use: "daemon", |
| 19 | + Short: "Manage gateway daemon service lifecycle", |
| 20 | +} |
| 21 | + |
| 22 | +var daemonInstallCmd = &cobra.Command{ |
| 23 | + Use: "install", |
| 24 | + Short: "Install and optionally activate systemd service (Linux)", |
| 25 | + RunE: runDaemonInstall, |
| 26 | +} |
| 27 | + |
| 28 | +var daemonUninstallCmd = &cobra.Command{ |
| 29 | + Use: "uninstall", |
| 30 | + Short: "Disable and remove systemd service (Linux)", |
| 31 | + RunE: runDaemonUninstall, |
| 32 | +} |
| 33 | + |
| 34 | +var daemonStartCmd = &cobra.Command{ |
| 35 | + Use: "start", |
| 36 | + Short: "Start systemd service (Linux)", |
| 37 | + RunE: runDaemonStart, |
| 38 | +} |
| 39 | + |
| 40 | +var daemonStopCmd = &cobra.Command{ |
| 41 | + Use: "stop", |
| 42 | + Short: "Stop systemd service (Linux)", |
| 43 | + RunE: runDaemonStop, |
| 44 | +} |
| 45 | + |
| 46 | +var daemonRestartCmd = &cobra.Command{ |
| 47 | + Use: "restart", |
| 48 | + Short: "Restart systemd service (Linux)", |
| 49 | + RunE: runDaemonRestart, |
| 50 | +} |
| 51 | + |
| 52 | +var daemonStatusCmd = &cobra.Command{ |
| 53 | + Use: "status", |
| 54 | + Short: "Show systemd service status (Linux)", |
| 55 | + RunE: runDaemonStatus, |
| 56 | +} |
| 57 | + |
| 58 | +var daemonServiceUser string |
| 59 | +var daemonServiceBinary string |
| 60 | +var daemonServicePort int |
| 61 | +var daemonInstallRoot string |
| 62 | +var daemonRuntimeLabel string |
| 63 | +var daemonActivate bool |
| 64 | +var daemonJSON bool |
| 65 | + |
| 66 | +var daemonExecFn = func(name string, args ...string) ([]byte, error) { |
| 67 | + return exec.Command(name, args...).CombinedOutput() |
| 68 | +} |
| 69 | +var daemonOS = runtime.GOOS |
| 70 | +var daemonCurrentEUID = os.Geteuid |
| 71 | +var daemonSetupSystemdFn = onboarding.SetupSystemdGateway |
| 72 | +var daemonActivateSystemdFn = onboarding.ActivateSystemdGateway |
| 73 | + |
| 74 | +func init() { |
| 75 | + daemonInstallCmd.Flags().StringVar(&daemonServiceUser, "service-user", "kafclaw", "Service user for systemd unit") |
| 76 | + daemonInstallCmd.Flags().StringVar(&daemonServiceBinary, "service-binary", "/usr/local/bin/kafclaw", "kafclaw binary path in ExecStart") |
| 77 | + daemonInstallCmd.Flags().IntVar(&daemonServicePort, "service-port", 18790, "Gateway port for systemd ExecStart") |
| 78 | + daemonInstallCmd.Flags().StringVar(&daemonInstallRoot, "service-install-root", "/", "Root path for systemd files (testing/packaging)") |
| 79 | + daemonInstallCmd.Flags().StringVar(&daemonRuntimeLabel, "daemon-runtime", "native", "Daemon runtime label persisted in config") |
| 80 | + daemonInstallCmd.Flags().BoolVar(&daemonActivate, "activate", true, "Run daemon-reload and enable --now after install") |
| 81 | + daemonInstallCmd.Flags().BoolVar(&daemonJSON, "json", false, "Output machine-readable JSON") |
| 82 | + _ = daemonInstallCmd.Flags().MarkHidden("service-install-root") |
| 83 | + |
| 84 | + daemonUninstallCmd.Flags().BoolVar(&daemonJSON, "json", false, "Output machine-readable JSON") |
| 85 | + daemonStartCmd.Flags().BoolVar(&daemonJSON, "json", false, "Output machine-readable JSON") |
| 86 | + daemonStopCmd.Flags().BoolVar(&daemonJSON, "json", false, "Output machine-readable JSON") |
| 87 | + daemonRestartCmd.Flags().BoolVar(&daemonJSON, "json", false, "Output machine-readable JSON") |
| 88 | + daemonStatusCmd.Flags().BoolVar(&daemonJSON, "json", false, "Output machine-readable JSON") |
| 89 | + |
| 90 | + daemonCmd.AddCommand(daemonInstallCmd, daemonUninstallCmd, daemonStartCmd, daemonStopCmd, daemonRestartCmd, daemonStatusCmd) |
| 91 | + rootCmd.AddCommand(daemonCmd) |
| 92 | +} |
| 93 | + |
| 94 | +func runDaemonInstall(cmd *cobra.Command, args []string) error { |
| 95 | + if daemonOS != "linux" { |
| 96 | + return daemonResult(cmd, "error", "install", map[string]any{"os": daemonOS}, "daemon install currently supports Linux systemd only") |
| 97 | + } |
| 98 | + cfg, err := config.Load() |
| 99 | + if err != nil { |
| 100 | + return daemonResult(cmd, "error", "install", nil, fmt.Sprintf("load config: %v", err)) |
| 101 | + } |
| 102 | + if daemonServicePort <= 0 { |
| 103 | + if cfg.Gateway.Port > 0 { |
| 104 | + daemonServicePort = cfg.Gateway.Port |
| 105 | + } else { |
| 106 | + daemonServicePort = 18790 |
| 107 | + } |
| 108 | + } |
| 109 | + cfg.Gateway.DaemonRuntime = strings.TrimSpace(daemonRuntimeLabel) |
| 110 | + if cfg.Gateway.DaemonRuntime == "" { |
| 111 | + cfg.Gateway.DaemonRuntime = "native" |
| 112 | + } |
| 113 | + if err := config.Save(cfg); err != nil { |
| 114 | + return daemonResult(cmd, "error", "install", nil, fmt.Sprintf("save config: %v", err)) |
| 115 | + } |
| 116 | + |
| 117 | + result, err := daemonSetupSystemdFn(onboarding.SetupOptions{ |
| 118 | + ServiceUser: daemonServiceUser, |
| 119 | + BinaryPath: daemonServiceBinary, |
| 120 | + Port: daemonServicePort, |
| 121 | + Profile: "default", |
| 122 | + Version: version, |
| 123 | + InstallRoot: daemonInstallRoot, |
| 124 | + }) |
| 125 | + if err != nil { |
| 126 | + return daemonResult(cmd, "error", "install", nil, err.Error()) |
| 127 | + } |
| 128 | + if daemonActivate { |
| 129 | + if daemonCurrentEUID() != 0 { |
| 130 | + return daemonResult(cmd, "error", "install", map[string]any{"servicePath": result.ServicePath}, "activation requires root privileges") |
| 131 | + } |
| 132 | + if err := daemonActivateSystemdFn(); err != nil { |
| 133 | + return daemonResult(cmd, "error", "install", map[string]any{"servicePath": result.ServicePath}, err.Error()) |
| 134 | + } |
| 135 | + } |
| 136 | + return daemonResult(cmd, "ok", "install", map[string]any{ |
| 137 | + "servicePath": result.ServicePath, |
| 138 | + "overridePath": result.OverridePath, |
| 139 | + "envPath": result.EnvPath, |
| 140 | + "userCreated": result.UserCreated, |
| 141 | + "activated": daemonActivate, |
| 142 | + "runtime": cfg.Gateway.DaemonRuntime, |
| 143 | + }, "") |
| 144 | +} |
| 145 | + |
| 146 | +func runDaemonUninstall(cmd *cobra.Command, args []string) error { |
| 147 | + if daemonOS != "linux" { |
| 148 | + return daemonResult(cmd, "error", "uninstall", map[string]any{"os": daemonOS}, "daemon uninstall currently supports Linux systemd only") |
| 149 | + } |
| 150 | + if daemonCurrentEUID() != 0 { |
| 151 | + return daemonResult(cmd, "error", "uninstall", nil, "uninstall requires root privileges") |
| 152 | + } |
| 153 | + _, _ = daemonExecFn("systemctl", "disable", "--now", "kafclaw-gateway.service") |
| 154 | + _ = os.Remove(filepath.Join("/", "etc", "systemd", "system", "kafclaw-gateway.service")) |
| 155 | + _, _ = daemonExecFn("systemctl", "daemon-reload") |
| 156 | + return daemonResult(cmd, "ok", "uninstall", nil, "") |
| 157 | +} |
| 158 | + |
| 159 | +func runDaemonStart(cmd *cobra.Command, args []string) error { |
| 160 | + return runDaemonSystemctlAction(cmd, "start") |
| 161 | +} |
| 162 | + |
| 163 | +func runDaemonStop(cmd *cobra.Command, args []string) error { |
| 164 | + return runDaemonSystemctlAction(cmd, "stop") |
| 165 | +} |
| 166 | + |
| 167 | +func runDaemonRestart(cmd *cobra.Command, args []string) error { |
| 168 | + return runDaemonSystemctlAction(cmd, "restart") |
| 169 | +} |
| 170 | + |
| 171 | +func runDaemonSystemctlAction(cmd *cobra.Command, action string) error { |
| 172 | + if daemonOS != "linux" { |
| 173 | + return daemonResult(cmd, "error", action, map[string]any{"os": daemonOS}, "daemon actions currently support Linux systemd only") |
| 174 | + } |
| 175 | + if daemonCurrentEUID() != 0 { |
| 176 | + return daemonResult(cmd, "error", action, nil, fmt.Sprintf("%s requires root privileges", action)) |
| 177 | + } |
| 178 | + out, err := daemonExecFn("systemctl", action, "kafclaw-gateway.service") |
| 179 | + if err != nil { |
| 180 | + return daemonResult(cmd, "error", action, map[string]any{"output": strings.TrimSpace(string(out))}, err.Error()) |
| 181 | + } |
| 182 | + return daemonResult(cmd, "ok", action, map[string]any{"output": strings.TrimSpace(string(out))}, "") |
| 183 | +} |
| 184 | + |
| 185 | +func runDaemonStatus(cmd *cobra.Command, args []string) error { |
| 186 | + if daemonOS != "linux" { |
| 187 | + return daemonResult(cmd, "error", "status", map[string]any{"os": daemonOS}, "daemon status currently supports Linux systemd only") |
| 188 | + } |
| 189 | + enabledOut, enabledErr := daemonExecFn("systemctl", "is-enabled", "kafclaw-gateway.service") |
| 190 | + activeOut, activeErr := daemonExecFn("systemctl", "is-active", "kafclaw-gateway.service") |
| 191 | + result := map[string]any{ |
| 192 | + "enabled": strings.TrimSpace(string(enabledOut)), |
| 193 | + "active": strings.TrimSpace(string(activeOut)), |
| 194 | + } |
| 195 | + if enabledErr != nil || activeErr != nil { |
| 196 | + return daemonResult(cmd, "error", "status", result, "service not enabled/active") |
| 197 | + } |
| 198 | + return daemonResult(cmd, "ok", "status", result, "") |
| 199 | +} |
| 200 | + |
| 201 | +func daemonResult(cmd *cobra.Command, status, action string, result map[string]any, errMsg string) error { |
| 202 | + if daemonJSON { |
| 203 | + payload := map[string]any{ |
| 204 | + "status": strings.TrimSpace(status), |
| 205 | + "command": "daemon", |
| 206 | + "action": strings.TrimSpace(action), |
| 207 | + } |
| 208 | + if len(result) > 0 { |
| 209 | + payload["result"] = result |
| 210 | + } |
| 211 | + if strings.TrimSpace(errMsg) != "" { |
| 212 | + payload["error"] = strings.TrimSpace(errMsg) |
| 213 | + } |
| 214 | + b, _ := json.MarshalIndent(payload, "", " ") |
| 215 | + fmt.Fprintln(cmd.OutOrStdout(), string(b)) |
| 216 | + if strings.EqualFold(status, "error") { |
| 217 | + return fmt.Errorf("%s", errMsg) |
| 218 | + } |
| 219 | + return nil |
| 220 | + } |
| 221 | + if strings.EqualFold(status, "error") { |
| 222 | + return fmt.Errorf("%s", errMsg) |
| 223 | + } |
| 224 | + fmt.Fprintf(cmd.OutOrStdout(), "daemon %s: ok\n", action) |
| 225 | + return nil |
| 226 | +} |
0 commit comments