Files
backend/os.go
NanamiAdmin da729b44ff feat(service): add auto-start management for services
Implement setBootAtStart and removeBootAtStart functions to handle service auto-start configuration separately from service creation/removal. Update handlers to use these new functions for better control of boot behavior. The changes support Windows, systemd and init.d systems.
2026-03-25 11:05:17 +08:00

585 lines
16 KiB
Go

package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"super-frpc/postLog"
)
func GetConfigDir() (string, error) {
config, err := GetConfig()
if err != nil {
postLog.Error(fmt.Sprintf("[GetConfigDir] Failed to get config: %v", err))
return "", err
}
return config.InstancePath, nil
}
func GetFrpcPath() (string, error) {
config, err := GetConfig()
if err != nil {
postLog.Error(fmt.Sprintf("[GetFrpcPath] Failed to get config: %v", err))
return "", err
}
return config.FrpcPath, nil
}
func GetInitSystem() string {
if runtime.GOOS == "windows" {
return "windows"
}
if runtime.GOOS == "linux" {
if _, err := os.Stat("/run/systemd/system"); err == nil {
return "systemd"
}
if _, err := os.Stat("/etc/init.d"); err == nil {
return "init.d"
}
}
return "unknown"
}
func createBootService(username, instanceName, configPath, runUser string) error {
initType := GetInitSystem()
switch initType {
case "systemd":
return createSystemdService(username, instanceName, configPath, runUser)
case "init.d":
return createInitDService(username, instanceName, configPath, runUser)
case "windows":
return createWindowsBootService(username, instanceName, configPath)
default:
return fmt.Errorf("unsupported init system: %s", initType)
}
}
func createSystemdService(username, instanceName, configPath, runUser string) error {
frpcPath, err := GetFrpcPath()
if err != nil {
return err
}
serviceName := fmt.Sprintf("superfrpc_%s_%s", username, instanceName)
serviceContent := fmt.Sprintf(`[Unit]
Description=superfrpc_%s_%s
After=network.target
[Service]
Type=simple
ExecStart=%s -c %s
User=%s
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
`, username, instanceName, frpcPath, configPath, runUser)
servicePath := filepath.Join("/etc/systemd/system", serviceName+".service")
if err := os.WriteFile(servicePath, []byte(serviceContent), 0644); err != nil {
return fmt.Errorf("failed to create systemd service file: %w", err)
}
cmd := exec.Command("systemctl", "enable", serviceName)
if output, err := cmd.CombinedOutput(); err != nil {
os.Remove(servicePath)
return fmt.Errorf("failed to enable service: %s, output: %s", err, output)
}
cmd = exec.Command("systemctl", "daemon-reload")
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to reload daemon: %s, output: %s", err, output)
}
return nil
}
func createInitDService(username, instanceName, configPath, runUser string) error {
frpcPath, err := GetFrpcPath()
if err != nil {
return err
}
serviceName := fmt.Sprintf("superfrpc_%s_%s", username, instanceName)
var serviceContent string
runUserArg := ""
if runUser != "" && runUser != "root" {
runUserArg = runUser
serviceContent = fmt.Sprintf(`#!/bin/sh
### BEGIN INIT INFO
# Provides: %s
# Required-Start: $network $remote_fs $syslog
# Required-Stop: $network $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Description: superfrpc %s %s
### END INIT INFO
NAME="%s"
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DAEMON=%s
DAEMON_ARGS="-c %s"
SCRIPTNAME=/etc/init.d/$NAME
USER=%s
[ -x "$DAEMON" ] || exit 0
case "$1" in
start)
echo -n "Starting $NAME: "
start-stop-daemon -S -c $USER -b -m -p /var/run/$NAME.pid --start --exec $DAEMON -- $DAEMON_ARGS
echo "$NAME."
;;
stop)
echo -n "Stopping $NAME: "
start-stop-daemon -K -p /var/run/$NAME.pid
rm -f /var/run/$NAME.pid
echo "$NAME."
;;
restart)
$0 stop
$0 start
;;
status)
if [ -f /var/run/$NAME.pid ]; then
echo "$NAME is running (pid $(cat /var/run/$NAME.pid))"
else
echo "$NAME is not running"
fi
;;
*)
echo "Usage: $SCRIPTNAME {start|stop|restart|status}"
exit 1
;;
esac
exit 0
`, serviceName, username, instanceName, serviceName, frpcPath, configPath, runUserArg)
} else {
serviceContent = fmt.Sprintf(`#!/bin/sh
### BEGIN INIT INFO
# Provides: %s
# Required-Start: $network $remote_fs $syslog
# Required-Stop: $network $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Description: superfrpc %s %s
### END INIT INFO
NAME="%s"
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DAEMON=%s
DAEMON_ARGS="-c %s"
SCRIPTNAME=/etc/init.d/$NAME
[ -x "$DAEMON" ] || exit 0
case "$1" in
start)
echo -n "Starting $NAME: "
start-stop-daemon -S -b -m -p /var/run/$NAME.pid --start --exec $DAEMON -- $DAEMON_ARGS
echo "$NAME."
;;
stop)
echo -n "Stopping $NAME: "
start-stop-daemon -K -p /var/run/$NAME.pid
rm -f /var/run/$NAME.pid
echo "$NAME."
;;
restart)
$0 stop
$0 start
;;
status)
if [ -f /var/run/$NAME.pid ]; then
echo "$NAME is running (pid $(cat /var/run/$NAME.pid))"
else
echo "$NAME is not running"
fi
;;
*)
echo "Usage: $SCRIPTNAME {start|stop|restart|status}"
exit 1
;;
esac
exit 0
`, serviceName, username, instanceName, serviceName, frpcPath, configPath)
}
servicePath := filepath.Join("/etc/init.d", serviceName)
if err := os.WriteFile(servicePath, []byte(serviceContent), 0755); err != nil {
return fmt.Errorf("failed to create init.d service file: %w", err)
}
cmd := exec.Command("/etc/init.d", serviceName, "enable")
if output, err := cmd.CombinedOutput(); err != nil {
os.Remove(servicePath)
return fmt.Errorf("failed to enable service: %s, output: %s", err, output)
}
return nil
}
func createWindowsBootService(username, instanceName, configPath string) error {
frpcPath, err := GetFrpcPath()
if err != nil {
return err
}
serviceName := fmt.Sprintf("superfrpc_%s_%s", username, instanceName)
command := fmt.Sprintf("\"%s\" -c \"%s\"", frpcPath, configPath)
cmd := exec.Command("sc", "create", serviceName, "binPath=", command, "start=", "auto")
output, err := cmd.CombinedOutput()
postLog.Debug(fmt.Sprintf("[createWindowsBootService] Sc create output: %s", output))
if err != nil {
return fmt.Errorf("failed to create Windows service: %s, output: %s", err, output)
}
return nil
}
func setBootAtStart(username, instanceName string) error {
initType := GetInitSystem()
serviceName := fmt.Sprintf("superfrpc_%s_%s", username, instanceName)
switch initType {
case "windows":
cmd := exec.Command("sc", "config", serviceName, "start=", "auto")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to set Windows service %s to auto-start: %s, output: %s", serviceName, err, output)
}
return nil
case "systemd":
cmd := exec.Command("systemctl", "enable", serviceName)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to enable systemd service %s: %s, output: %s", serviceName, err, output)
}
return nil
case "init.d":
cmd := exec.Command("update-rc.d", serviceName, "enable")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to enable init.d service %s: %s, output: %s", serviceName, err, output)
}
return nil
default:
return fmt.Errorf("unsupported init system: %s", initType)
}
}
func removeBootAtStart(username, instanceName string) error {
initType := GetInitSystem()
serviceName := fmt.Sprintf("superfrpc_%s_%s", username, instanceName)
switch initType{
case "windows":
cmd := exec.Command("sc", "config", serviceName, "start=", "disabled")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to set Windows service %s to disabled-start: %s, output: %s", serviceName, err, output)
}
return nil
case "systemd":
cmd := exec.Command("systemctl", "disable", serviceName)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to disable systemd service %s: %s, output: %s", serviceName, err, output)
}
return nil
case "init.d":
cmd := exec.Command("update-rc.d", serviceName, "disable")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to disable init.d service %s: %s, output: %s", serviceName, err, output)
}
return nil
default:
return fmt.Errorf("unsupported init system: %s", initType)
}
}
func removeBootService(username, instanceName string) error {
initType := GetInitSystem()
serviceName := fmt.Sprintf("superfrpc_%s_%s", username, instanceName)
switch initType {
case "systemd":
cmd := exec.Command("systemctl", "disable", serviceName)
cmd.CombinedOutput()
servicePath := filepath.Join("/etc/systemd/system", serviceName+".service")
os.Remove(servicePath)
cmd = exec.Command("systemctl", "daemon-reload")
cmd.CombinedOutput()
case "init.d":
cmd := exec.Command("/etc/init.d", serviceName, "disable")
cmd.CombinedOutput()
servicePath := filepath.Join("/etc/init.d", serviceName)
os.Remove(servicePath)
case "windows":
cmd := exec.Command("sc", "delete", serviceName)
postLog.Debug(fmt.Sprintf("[removeBootService] Sc delete command: %s", cmd.String()))
if output, err := cmd.CombinedOutput(); err != nil {
postLog.Error(fmt.Sprintf("[removeBootService] Failed to delete Windows service: %s, output: %s", err, output))
}
default:
postLog.Error(fmt.Sprintf("[removeBootService] Unsupported init system: %s", initType))
}
return nil
}
func IsInstanceRunning(instanceName string) bool {
initType := GetInitSystem()
switch initType {
case "windows":
cmd := exec.Command("sc", "query", instanceName)
output, err := cmd.CombinedOutput()
postLog.Debug(fmt.Sprintf("[IsInstanceRunning] Sc query output: %s", output))
if err != nil {
postLog.Error(fmt.Sprintf("[IsInstanceRunning] Failed to check Windows service status: %s, output: %s", err, output))
return false
}
outputStr := string(output)
if strings.Contains(outputStr, "RUNNING") {
postLog.Info(fmt.Sprintf("[IsInstanceRunning] Windows service %s is running", instanceName))
return true
} else {
postLog.Info(fmt.Sprintf("[IsInstanceRunning] Windows service %s is stopped", instanceName))
return false
}
case "systemd":
cmd := exec.Command("systemctl", "is-active", instanceName)
output, err := cmd.CombinedOutput()
if err != nil {
postLog.Error(fmt.Sprintf("[IsInstanceRunning] Failed to check systemd service status: %s, output: %s", err, output))
return false
}
status := strings.TrimSpace(string(output))
if status == "active" {
postLog.Info(fmt.Sprintf("[IsInstanceRunning] Systemd service %s is running", instanceName))
return true
} else {
postLog.Info(fmt.Sprintf("[IsInstanceRunning] Systemd service %s is stopped", instanceName))
return false
}
case "init.d":
servicePath := fmt.Sprintf("/etc/init.d/%s", instanceName)
if _, err := os.Stat(servicePath); err != nil {
postLog.Info(fmt.Sprintf("[IsInstanceRunning] Init.d service %s not found", instanceName))
return false
}
cmd := exec.Command(servicePath, "status")
output, err := cmd.CombinedOutput()
if err != nil {
postLog.Error(fmt.Sprintf("[IsInstanceRunning] Failed to check init.d service status: %s, output: %s", err, output))
return false
}
outputStr := strings.TrimSpace(string(output))
if strings.Contains(outputStr, "is running") {
return true
} else {
return false
}
default:
postLog.Error(fmt.Sprintf("[IsInstanceRunning] Unsupported init system: %s", initType))
return false
}
}
func GetInstancePid(instanceName string) int {
initType := GetInitSystem()
switch initType {
case "windows":
cmd := exec.Command("sc", "query", instanceName, "info")
output, err := cmd.CombinedOutput()
if err != nil {
postLog.Error(fmt.Sprintf("[GetInstancePid] Failed to get Windows service info: %s, output: %s", err, output))
return 0
}
outputStr := string(output)
lines := strings.Split(outputStr, "\n")
for _, line := range lines {
if strings.Contains(line, "PID") {
parts := strings.Split(line, ":")
if len(parts) >= 2 {
pidStr := strings.TrimSpace(parts[1])
pid, err := strconv.Atoi(pidStr)
if err == nil {
return pid
}
}
}
}
return 0
case "systemd":
cmd := exec.Command("systemctl", "show", instanceName, "--property", "MainPID")
output, err := cmd.CombinedOutput()
if err != nil {
postLog.Error(fmt.Sprintf("[GetInstancePid] Failed to get PID from systemd: %s, output: %s", err, output))
return 0
}
outputStr := strings.TrimSpace(string(output))
parts := strings.Split(outputStr, "=")
if len(parts) >= 2 {
pidStr := strings.TrimSpace(parts[1])
if pidStr != "" && pidStr != "0" {
pid, err := strconv.Atoi(pidStr)
if err == nil {
return pid
}
}
}
return 0
case "init.d":
servicePath := fmt.Sprintf("/etc/init.d/%s", instanceName)
if _, err := os.Stat(servicePath); err != nil {
postLog.Error(fmt.Sprintf("[GetInstancePid] Init.d service %s not found", instanceName))
return 0
}
cmd := exec.Command(servicePath, "status")
output, err := cmd.CombinedOutput()
if err != nil {
postLog.Error(fmt.Sprintf("[GetInstancePid] Failed to get init.d service status: %s, output: %s", err, output))
return 0
}
outputStr := strings.TrimSpace(string(output))
if strings.Contains(outputStr, "is running") {
if strings.Contains(outputStr, "pid") {
parts := strings.Split(outputStr, "pid")
if len(parts) >= 2 {
pidStr := strings.TrimSpace(strings.Split(parts[1], ")")[0])
pid, err := strconv.Atoi(pidStr)
if err == nil {
return pid
}
}
}
pidFile := fmt.Sprintf("/var/run/%s.pid", instanceName)
if content, err := os.ReadFile(pidFile); err == nil {
pidStr := strings.TrimSpace(string(content))
pid, err := strconv.Atoi(pidStr)
if err == nil {
return pid
}
}
}
return 0
default:
postLog.Error(fmt.Sprintf("[GetInstancePid] Unsupported init system: %s", initType))
return 0
}
}
func StartWindowsService(instanceName string) error {
cmd := exec.Command("sc", "start", instanceName)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to start Windows service %s: %s, output: %s", instanceName, err, output)
}
return nil
}
func StartSystemdService(instanceName string) error {
cmd := exec.Command("systemctl", "start", instanceName)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to start systemd service %s: %s, output: %s", instanceName, err, output)
}
return nil
}
func StartInitDService(instanceName string) error {
servicePath := fmt.Sprintf("/etc/init.d/%s", instanceName)
cmd := exec.Command(servicePath, "start")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to start init.d service %s: %s, output: %s", instanceName, err, output)
}
return nil
}
func StopWindowsService(instanceName string) error {
cmd := exec.Command("sc", "stop", instanceName)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to stop Windows service %s: %s, output: %s", instanceName, err, output)
}
return nil
}
func StopSystemdService(instanceName string) error {
cmd := exec.Command("systemctl", "stop", instanceName)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to stop systemd service %s: %s, output: %s", instanceName, err, output)
}
return nil
}
func StopInitDService(instanceName string) error {
servicePath := fmt.Sprintf("/etc/init.d/%s", instanceName)
cmd := exec.Command(servicePath, "stop")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to stop init.d service %s: %s, output: %s", instanceName, err, output)
}
return nil
}
func RestartWindowsService(instanceName string) error {
cmd := exec.Command("sc", "restart", instanceName)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to restart Windows service %s: %s, output: %s", instanceName, err, output)
}
return nil
}
func RestartSystemdService(instanceName string) error {
cmd := exec.Command("systemctl", "restart", instanceName)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to restart systemd service %s: %s, output: %s", instanceName, err, output)
}
return nil
}
func RestartInitDService(instanceName string) error {
servicePath := fmt.Sprintf("/etc/init.d/%s", instanceName)
cmd := exec.Command(servicePath, "restart")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to restart init.d service %s: %s, output: %s", instanceName, err, output)
}
return nil
}