|
| 1 | +// Package setup contains helpers for boot-time autostart configuration. |
| 2 | +// Keeping autostart logic here keeps the main package focused on runtime wiring. |
| 3 | +package setup |
| 4 | + |
| 5 | +import ( |
| 6 | + "bufio" |
| 7 | + "fmt" |
| 8 | + "os" |
| 9 | + "os/exec" |
| 10 | + "path/filepath" |
| 11 | + "strings" |
| 12 | + "time" |
| 13 | +) |
| 14 | + |
| 15 | +// linuxInfo keeps distribution details so we can surface them while deciding on init systems. |
| 16 | +// Capturing this data makes the operator aware of what we detected without guessing. |
| 17 | +type linuxInfo struct { |
| 18 | + ID string |
| 19 | + VersionID string |
| 20 | +} |
| 21 | + |
| 22 | +// ----- Autostart entrypoints ----- |
| 23 | + |
| 24 | +// OfferAutostartSetup selects the appropriate init system and guides the operator through setup. |
| 25 | +// The function keeps user prompts sequential while delegating long-running work to helpers. |
| 26 | +func OfferAutostartSetup(appName string, interactive *InteractiveResult, rotation time.Duration) (*SystemdResult, error) { |
| 27 | + reader := bufio.NewReader(os.Stdin) |
| 28 | + |
| 29 | + info := readLinuxInfo() |
| 30 | + if info.ID != "" || info.VersionID != "" { |
| 31 | + fmt.Printf("Detected Linux distribution: %s %s\n", info.ID, info.VersionID) |
| 32 | + } |
| 33 | + |
| 34 | + systemdAvailable := isSystemdAvailable() |
| 35 | + initAvailable := isInitAvailable() |
| 36 | + |
| 37 | + if systemdAvailable { |
| 38 | + fmt.Println("Systemd detected, offering systemd autostart setup.") |
| 39 | + return OfferSystemdSetup(appName, interactive, rotation) |
| 40 | + } |
| 41 | + |
| 42 | + if initAvailable { |
| 43 | + fmt.Println("Systemd not found, using legacy init script setup.") |
| 44 | + return OfferInitSetup(appName, interactive, rotation, reader) |
| 45 | + } |
| 46 | + |
| 47 | + fmt.Println("No supported init system detected; skipping autostart configuration.") |
| 48 | + return &SystemdResult{FollowLogs: false}, nil |
| 49 | +} |
| 50 | + |
| 51 | +// ----- Legacy init workflow ----- |
| 52 | + |
| 53 | +// OfferInitSetup creates a SysV-style init script and optionally enables and starts it. |
| 54 | +// Using a shared reader keeps the input flow consistent with systemd setup. |
| 55 | +func OfferInitSetup(appName string, interactive *InteractiveResult, rotation time.Duration, reader *bufio.Reader) (*SystemdResult, error) { |
| 56 | + fmt.Printf("Would you like to create a legacy init script for '%s'? (y/N): ", interactive.ServiceName) |
| 57 | + createAnswer, err := readTrimmed(reader) |
| 58 | + if err != nil { |
| 59 | + return nil, err |
| 60 | + } |
| 61 | + if strings.ToLower(createAnswer) != "y" { |
| 62 | + return &SystemdResult{FollowLogs: false}, nil |
| 63 | + } |
| 64 | + |
| 65 | + executable, err := os.Executable() |
| 66 | + if err != nil { |
| 67 | + return nil, fmt.Errorf("failed to resolve executable path: %v", err) |
| 68 | + } |
| 69 | + |
| 70 | + initName := initServiceName(interactive.ServiceName) |
| 71 | + scriptContent := buildInitScript(appName, interactive, rotation, executable, initName) |
| 72 | + scriptPath := filepath.Join("/etc/init.d", initName) |
| 73 | + if err := os.WriteFile(scriptPath, []byte(scriptContent), 0755); err != nil { |
| 74 | + return nil, fmt.Errorf("failed to write init script: %v", err) |
| 75 | + } |
| 76 | + |
| 77 | + fmt.Print("Enable the init script so it starts on boot? (y/N): ") |
| 78 | + enableAnswer, err := readTrimmed(reader) |
| 79 | + if err != nil { |
| 80 | + return nil, err |
| 81 | + } |
| 82 | + |
| 83 | + if strings.ToLower(enableAnswer) == "y" { |
| 84 | + if err := enableInitScript(initName); err != nil { |
| 85 | + return nil, err |
| 86 | + } |
| 87 | + } |
| 88 | + |
| 89 | + fmt.Print("Start the service now? (y/N): ") |
| 90 | + startAnswer, err := readTrimmed(reader) |
| 91 | + if err != nil { |
| 92 | + return nil, err |
| 93 | + } |
| 94 | + |
| 95 | + if strings.ToLower(startAnswer) == "y" { |
| 96 | + if err := runInitCommand(initName, "start"); err != nil { |
| 97 | + return nil, err |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + fmt.Print("Follow the log file now? (y/N): ") |
| 102 | + followAnswer, err := readTrimmed(reader) |
| 103 | + if err != nil { |
| 104 | + return nil, err |
| 105 | + } |
| 106 | + |
| 107 | + return &SystemdResult{FollowLogs: strings.ToLower(followAnswer) == "y"}, nil |
| 108 | +} |
| 109 | + |
| 110 | +// ----- Detection helpers ----- |
| 111 | + |
| 112 | +// readLinuxInfo gathers ID and VERSION_ID from os-release for visibility. |
| 113 | +// Reading /etc/os-release is the most portable way to detect Linux distribution details. |
| 114 | +func readLinuxInfo() linuxInfo { |
| 115 | + paths := []string{"/etc/os-release", "/usr/lib/os-release"} |
| 116 | + for _, path := range paths { |
| 117 | + file, err := os.Open(path) |
| 118 | + if err != nil { |
| 119 | + continue |
| 120 | + } |
| 121 | + defer file.Close() |
| 122 | + |
| 123 | + info := linuxInfo{} |
| 124 | + scanner := bufio.NewScanner(file) |
| 125 | + for scanner.Scan() { |
| 126 | + line := scanner.Text() |
| 127 | + if strings.HasPrefix(line, "ID=") { |
| 128 | + info.ID = strings.Trim(strings.TrimPrefix(line, "ID="), `"`) |
| 129 | + } |
| 130 | + if strings.HasPrefix(line, "VERSION_ID=") { |
| 131 | + info.VersionID = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), `"`) |
| 132 | + } |
| 133 | + } |
| 134 | + return info |
| 135 | + } |
| 136 | + |
| 137 | + return linuxInfo{} |
| 138 | +} |
| 139 | + |
| 140 | +// ----- Init system probes ----- |
| 141 | + |
| 142 | +// isSystemdAvailable checks systemd presence via runtime paths and binaries. |
| 143 | +// Looking for both a runtime directory and systemctl makes detection more reliable. |
| 144 | +func isSystemdAvailable() bool { |
| 145 | + if _, err := os.Stat("/run/systemd/system"); err == nil { |
| 146 | + return true |
| 147 | + } |
| 148 | + if _, err := exec.LookPath("systemctl"); err == nil { |
| 149 | + return true |
| 150 | + } |
| 151 | + return false |
| 152 | +} |
| 153 | + |
| 154 | +// isInitAvailable checks for a legacy init system using common paths. |
| 155 | +// We keep the detection conservative to avoid writing scripts on unsupported systems. |
| 156 | +func isInitAvailable() bool { |
| 157 | + if _, err := os.Stat("/sbin/init"); err == nil { |
| 158 | + if _, err := os.Stat("/etc/init.d"); err == nil { |
| 159 | + return true |
| 160 | + } |
| 161 | + } |
| 162 | + return false |
| 163 | +} |
| 164 | + |
| 165 | +// ----- Script builders ----- |
| 166 | + |
| 167 | +// initServiceName removes a systemd suffix for init script naming. |
| 168 | +// Stripping the suffix keeps SysV script names aligned with common conventions. |
| 169 | +func initServiceName(serviceName string) string { |
| 170 | + return strings.TrimSuffix(serviceName, ".service") |
| 171 | +} |
| 172 | + |
| 173 | +// buildInitScript renders a SysV-style init script with start/stop commands. |
| 174 | +// Using a pidfile keeps lifecycle management simple without extra dependencies. |
| 175 | +func buildInitScript(appName string, interactive *InteractiveResult, rotation time.Duration, executable, initName string) string { |
| 176 | + args := buildArgs(interactive, rotation) |
| 177 | + |
| 178 | + return fmt.Sprintf(`#!/bin/sh |
| 179 | +### BEGIN INIT INFO |
| 180 | +# Provides: %s |
| 181 | +# Required-Start: $network |
| 182 | +# Required-Stop: $network |
| 183 | +# Default-Start: 2 3 4 5 |
| 184 | +# Default-Stop: 0 1 6 |
| 185 | +# Short-Description: %s proxy service |
| 186 | +### END INIT INFO |
| 187 | +
|
| 188 | +APP_NAME="%s" |
| 189 | +EXEC="%s" |
| 190 | +ARGS="%s" |
| 191 | +PIDFILE="/var/run/%s.pid" |
| 192 | +
|
| 193 | +start() { |
| 194 | + if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then |
| 195 | + echo "$APP_NAME is already running" |
| 196 | + return 0 |
| 197 | + fi |
| 198 | + echo "Starting $APP_NAME" |
| 199 | + nohup "$EXEC" $ARGS >/dev/null 2>&1 & |
| 200 | + echo $! > "$PIDFILE" |
| 201 | +} |
| 202 | +
|
| 203 | +stop() { |
| 204 | + if [ ! -f "$PIDFILE" ]; then |
| 205 | + echo "$APP_NAME is not running" |
| 206 | + return 0 |
| 207 | + fi |
| 208 | + PID="$(cat "$PIDFILE")" |
| 209 | + if kill -0 "$PID" 2>/dev/null; then |
| 210 | + echo "Stopping $APP_NAME" |
| 211 | + kill "$PID" |
| 212 | + fi |
| 213 | + rm -f "$PIDFILE" |
| 214 | +} |
| 215 | +
|
| 216 | +case "$1" in |
| 217 | + start) |
| 218 | + start |
| 219 | + ;; |
| 220 | + stop) |
| 221 | + stop |
| 222 | + ;; |
| 223 | + restart) |
| 224 | + stop |
| 225 | + start |
| 226 | + ;; |
| 227 | + status) |
| 228 | + if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then |
| 229 | + echo "$APP_NAME is running" |
| 230 | + else |
| 231 | + echo "$APP_NAME is stopped" |
| 232 | + fi |
| 233 | + ;; |
| 234 | + *) |
| 235 | + echo "Usage: $0 {start|stop|restart|status}" |
| 236 | + exit 1 |
| 237 | + ;; |
| 238 | +esac |
| 239 | +
|
| 240 | +exit 0 |
| 241 | +`, initName, appName, appName, executable, strings.Join(args, " "), initName) |
| 242 | +} |
| 243 | + |
| 244 | +// ----- Init system integration ----- |
| 245 | + |
| 246 | +// enableInitScript hooks the script into the default runlevels using available tools. |
| 247 | +// Supporting both update-rc.d and chkconfig keeps compatibility across distributions. |
| 248 | +func enableInitScript(initName string) error { |
| 249 | + if _, err := exec.LookPath("update-rc.d"); err == nil { |
| 250 | + return runCommand("update-rc.d", initName, "defaults") |
| 251 | + } |
| 252 | + if _, err := exec.LookPath("chkconfig"); err == nil { |
| 253 | + if err := runCommand("chkconfig", "--add", initName); err != nil { |
| 254 | + return err |
| 255 | + } |
| 256 | + return runCommand("chkconfig", initName, "on") |
| 257 | + } |
| 258 | + return fmt.Errorf("no init enablement tool found (update-rc.d or chkconfig)") |
| 259 | +} |
| 260 | + |
| 261 | +// runInitCommand executes the init script with the provided action. |
| 262 | +// Using exec.Command avoids shell interpretation while keeping output available. |
| 263 | +func runInitCommand(initName, action string) error { |
| 264 | + return runCommand(filepath.Join("/etc/init.d", initName), action) |
| 265 | +} |
| 266 | + |
| 267 | +// ----- Command execution ----- |
| 268 | + |
| 269 | +// runCommand executes a command and surfaces combined output on failure. |
| 270 | +// Returning detailed errors makes it easier for operators to diagnose issues. |
| 271 | +func runCommand(name string, args ...string) error { |
| 272 | + cmd := exec.Command(name, args...) |
| 273 | + output, err := cmd.CombinedOutput() |
| 274 | + if err != nil { |
| 275 | + return fmt.Errorf("%s %s failed: %v - %s", name, strings.Join(args, " "), err, string(output)) |
| 276 | + } |
| 277 | + return nil |
| 278 | +} |
| 279 | + |
| 280 | +// ----- Shared argument builder ----- |
| 281 | + |
| 282 | +// buildArgs renders CLI flags for systemd or init scripts. |
| 283 | +// Having a single formatter ensures consistent startup arguments. |
| 284 | +func buildArgs(interactive *InteractiveResult, rotation time.Duration) []string { |
| 285 | + args := make([]string, 0) |
| 286 | + if interactive.RoutesFlag != "" { |
| 287 | + args = append(args, fmt.Sprintf("-routes=%s", interactive.RoutesFlag)) |
| 288 | + } |
| 289 | + if interactive.UDPRoutesFlag != "" { |
| 290 | + args = append(args, fmt.Sprintf("-udp-routes=%s", interactive.UDPRoutesFlag)) |
| 291 | + } |
| 292 | + args = append(args, fmt.Sprintf("-log=%s", interactive.LogFile)) |
| 293 | + args = append(args, fmt.Sprintf("-rotation=%s", rotation.String())) |
| 294 | + return args |
| 295 | +} |
0 commit comments