Skip to content

Commit 4c502bc

Browse files
authored
stable release
Add autostart detection and legacy SysV init-script fallback
2 parents e618937 + 704cc34 commit 4c502bc

3 files changed

Lines changed: 297 additions & 10 deletions

File tree

chicha-ip-proxy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func main() {
6666
*routesFlag = interactiveResult.RoutesFlag
6767
*udpRoutesFlag = interactiveResult.UDPRoutesFlag
6868

69-
systemdResult, err = setup.OfferSystemdSetup("chicha-ip-proxy", interactiveResult, *rotationFrequency)
69+
systemdResult, err = setup.OfferAutostartSetup("chicha-ip-proxy", interactiveResult, *rotationFrequency)
7070
if err != nil {
7171
log.Printf("Systemd setup encountered an issue: %v", err)
7272
}

pkg/setup/autostart.go

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
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+
}

pkg/setup/systemd.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,7 @@ func StreamLogs(logFile string, stop <-chan struct{}) {
116116
// buildUnitFile composes a systemd unit with explicit log file arguments and rotation schedule.
117117
// Embedding the rotation flag keeps the service aligned with interactive defaults.
118118
func buildUnitFile(appName string, interactive *InteractiveResult, rotation time.Duration, executable string) string {
119-
args := make([]string, 0)
120-
if interactive.RoutesFlag != "" {
121-
args = append(args, fmt.Sprintf("-routes=%s", interactive.RoutesFlag))
122-
}
123-
if interactive.UDPRoutesFlag != "" {
124-
args = append(args, fmt.Sprintf("-udp-routes=%s", interactive.UDPRoutesFlag))
125-
}
126-
args = append(args, fmt.Sprintf("-log=%s", interactive.LogFile))
127-
args = append(args, fmt.Sprintf("-rotation=%s", rotation.String()))
119+
args := buildArgs(interactive, rotation)
128120

129121
return fmt.Sprintf(`[Unit]
130122
Description=%s proxy service

0 commit comments

Comments
 (0)