换成单集群模式
This commit is contained in:
160
EdgeHttpDNS/internal/utils/exec/cmd.go
Normal file
160
EdgeHttpDNS/internal/utils/exec/cmd.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package executils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Cmd struct {
|
||||
name string
|
||||
args []string
|
||||
env []string
|
||||
dir string
|
||||
|
||||
ctx context.Context
|
||||
timeout time.Duration
|
||||
cancelFunc func()
|
||||
|
||||
captureStdout bool
|
||||
captureStderr bool
|
||||
|
||||
stdout *bytes.Buffer
|
||||
stderr *bytes.Buffer
|
||||
|
||||
rawCmd *exec.Cmd
|
||||
}
|
||||
|
||||
func NewCmd(name string, args ...string) *Cmd {
|
||||
return &Cmd{
|
||||
name: name,
|
||||
args: args,
|
||||
}
|
||||
}
|
||||
|
||||
func NewTimeoutCmd(timeout time.Duration, name string, args ...string) *Cmd {
|
||||
return (&Cmd{
|
||||
name: name,
|
||||
args: args,
|
||||
}).WithTimeout(timeout)
|
||||
}
|
||||
|
||||
func (this *Cmd) WithTimeout(timeout time.Duration) *Cmd {
|
||||
this.timeout = timeout
|
||||
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
|
||||
this.ctx = ctx
|
||||
this.cancelFunc = cancelFunc
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithStdout() *Cmd {
|
||||
this.captureStdout = true
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithStderr() *Cmd {
|
||||
this.captureStderr = true
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithEnv(env []string) *Cmd {
|
||||
this.env = env
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithDir(dir string) *Cmd {
|
||||
this.dir = dir
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) Start() error {
|
||||
var cmd = this.compose()
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
func (this *Cmd) Wait() error {
|
||||
var cmd = this.compose()
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
func (this *Cmd) Run() error {
|
||||
if this.cancelFunc != nil {
|
||||
defer this.cancelFunc()
|
||||
}
|
||||
|
||||
var cmd = this.compose()
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (this *Cmd) RawStdout() string {
|
||||
if this.stdout != nil {
|
||||
return this.stdout.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (this *Cmd) Stdout() string {
|
||||
return strings.TrimSpace(this.RawStdout())
|
||||
}
|
||||
|
||||
func (this *Cmd) RawStderr() string {
|
||||
if this.stderr != nil {
|
||||
return this.stderr.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (this *Cmd) Stderr() string {
|
||||
return strings.TrimSpace(this.RawStderr())
|
||||
}
|
||||
|
||||
func (this *Cmd) String() string {
|
||||
if this.rawCmd != nil {
|
||||
return this.rawCmd.String()
|
||||
}
|
||||
var newCmd = exec.Command(this.name, this.args...)
|
||||
return newCmd.String()
|
||||
}
|
||||
|
||||
func (this *Cmd) Process() *os.Process {
|
||||
if this.rawCmd != nil {
|
||||
return this.rawCmd.Process
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *Cmd) compose() *exec.Cmd {
|
||||
if this.rawCmd != nil {
|
||||
return this.rawCmd
|
||||
}
|
||||
|
||||
if this.ctx != nil {
|
||||
this.rawCmd = exec.CommandContext(this.ctx, this.name, this.args...)
|
||||
} else {
|
||||
this.rawCmd = exec.Command(this.name, this.args...)
|
||||
}
|
||||
|
||||
if this.env != nil {
|
||||
this.rawCmd.Env = this.env
|
||||
}
|
||||
|
||||
if len(this.dir) > 0 {
|
||||
this.rawCmd.Dir = this.dir
|
||||
}
|
||||
|
||||
if this.captureStdout {
|
||||
this.stdout = &bytes.Buffer{}
|
||||
this.rawCmd.Stdout = this.stdout
|
||||
}
|
||||
if this.captureStderr {
|
||||
this.stderr = &bytes.Buffer{}
|
||||
this.rawCmd.Stderr = this.stderr
|
||||
}
|
||||
|
||||
return this.rawCmd
|
||||
}
|
||||
57
EdgeHttpDNS/internal/utils/exec/look_linux.go
Normal file
57
EdgeHttpDNS/internal/utils/exec/look_linux.go
Normal file
@@ -0,0 +1,57 @@
|
||||
//go:build linux
|
||||
|
||||
package executils
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// LookPath customize our LookPath() function, to work in broken $PATH environment variable
|
||||
func LookPath(file string) (string, error) {
|
||||
result, err := exec.LookPath(file)
|
||||
if err == nil && len(result) > 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// add common dirs contains executable files these may be excluded in $PATH environment variable
|
||||
var binPaths = []string{
|
||||
"/usr/sbin",
|
||||
"/usr/bin",
|
||||
"/usr/local/sbin",
|
||||
"/usr/local/bin",
|
||||
}
|
||||
|
||||
for _, binPath := range binPaths {
|
||||
var fullPath = binPath + string(os.PathSeparator) + file
|
||||
|
||||
stat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if stat.IsDir() {
|
||||
return "", syscall.EISDIR
|
||||
}
|
||||
|
||||
var mode = stat.Mode()
|
||||
if mode.IsDir() {
|
||||
return "", syscall.EISDIR
|
||||
}
|
||||
err = syscall.Faccessat(unix.AT_FDCWD, fullPath, unix.X_OK, unix.AT_EACCESS)
|
||||
if err == nil || (err != syscall.ENOSYS && err != syscall.EPERM) {
|
||||
return fullPath, err
|
||||
}
|
||||
if mode&0111 != 0 {
|
||||
return fullPath, nil
|
||||
}
|
||||
return "", fs.ErrPermission
|
||||
}
|
||||
|
||||
return "", &exec.Error{
|
||||
Name: file,
|
||||
Err: exec.ErrNotFound,
|
||||
}
|
||||
}
|
||||
9
EdgeHttpDNS/internal/utils/exec/look_others.go
Normal file
9
EdgeHttpDNS/internal/utils/exec/look_others.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build !linux
|
||||
|
||||
package executils
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func LookPath(file string) (string, error) {
|
||||
return exec.LookPath(file)
|
||||
}
|
||||
@@ -6,25 +6,107 @@ package utils
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
teaconst "github.com/TeaOSLab/EdgeHttpDNS/internal/const"
|
||||
executils "github.com/TeaOSLab/EdgeHttpDNS/internal/utils/exec"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
)
|
||||
|
||||
var systemdServiceFile = "/etc/systemd/system/" + teaconst.SystemdServiceName + ".service"
|
||||
var initServiceFile = "/etc/init.d/" + teaconst.SystemdServiceName
|
||||
|
||||
// Install 安装系统服务
|
||||
func (m *ServiceManager) Install(exePath string, args []string) error {
|
||||
if os.Getgid() != 0 {
|
||||
return errors.New("only root users can install the service")
|
||||
}
|
||||
|
||||
systemd, err := exec.LookPath("systemctl")
|
||||
systemd, err := executils.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return err
|
||||
// systemd 不可用,降级到 init.d
|
||||
return m.installInitService(exePath, args)
|
||||
}
|
||||
|
||||
desc := `[Unit]
|
||||
Description=GoEdge HTTPDNS Node Service
|
||||
return m.installSystemdService(systemd, exePath, args)
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (m *ServiceManager) Start() error {
|
||||
if os.Getgid() != 0 {
|
||||
return errors.New("only root users can start the service")
|
||||
}
|
||||
|
||||
// 优先检查 systemd
|
||||
if fileExists(systemdServiceFile) {
|
||||
systemd, err := executils.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return executils.NewTimeoutCmd(10*time.Second, systemd, "start", teaconst.SystemdServiceName+".service").Start()
|
||||
}
|
||||
|
||||
// 降级到 init.d
|
||||
if fileExists(initServiceFile) {
|
||||
return executils.NewTimeoutCmd(10*time.Second, "service", teaconst.ProcessName, "start").Start()
|
||||
}
|
||||
|
||||
return errors.New("no service file found, please install the service first")
|
||||
}
|
||||
|
||||
// Uninstall 删除服务
|
||||
func (m *ServiceManager) Uninstall() error {
|
||||
if os.Getgid() != 0 {
|
||||
return errors.New("only root users can uninstall the service")
|
||||
}
|
||||
|
||||
// systemd
|
||||
if fileExists(systemdServiceFile) {
|
||||
systemd, err := executils.LookPath("systemctl")
|
||||
if err == nil {
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, systemd, "stop", teaconst.SystemdServiceName+".service").Run()
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, systemd, "disable", teaconst.SystemdServiceName+".service").Run()
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, systemd, "daemon-reload").Run()
|
||||
}
|
||||
return os.Remove(systemdServiceFile)
|
||||
}
|
||||
|
||||
// init.d
|
||||
if fileExists(initServiceFile) {
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, "service", teaconst.ProcessName, "stop").Run()
|
||||
chkCmd, err := executils.LookPath("chkconfig")
|
||||
if err == nil {
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, chkCmd, "--del", teaconst.ProcessName).Run()
|
||||
}
|
||||
return os.Remove(initServiceFile)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// installSystemdService 安装 systemd 服务
|
||||
func (m *ServiceManager) installSystemdService(systemd, exePath string, args []string) error {
|
||||
shortName := teaconst.SystemdServiceName
|
||||
longName := "GoEdge HTTPDNS"
|
||||
|
||||
// 用 bash 包装启动命令,兼容路径含空格的场景
|
||||
var startCmd = exePath + " daemon"
|
||||
bashPath, _ := executils.LookPath("bash")
|
||||
if len(bashPath) > 0 {
|
||||
startCmd = bashPath + " -c \"" + startCmd + "\""
|
||||
}
|
||||
|
||||
desc := `### BEGIN INIT INFO
|
||||
# Provides: ` + shortName + `
|
||||
# Required-Start: $all
|
||||
# Required-Stop:
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop:
|
||||
# Short-Description: ` + longName + ` Service
|
||||
### END INIT INFO
|
||||
|
||||
[Unit]
|
||||
Description=` + longName + ` Node Service
|
||||
Before=shutdown.target
|
||||
After=network-online.target
|
||||
|
||||
@@ -32,34 +114,102 @@ After=network-online.target
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=1s
|
||||
ExecStart=` + exePath + ` daemon
|
||||
ExecStart=` + startCmd + `
|
||||
ExecStop=` + exePath + ` stop
|
||||
ExecReload=` + exePath + ` restart
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target`
|
||||
|
||||
err = os.WriteFile(systemdServiceFile, []byte(desc), 0777)
|
||||
// 权限 0644,systemd 单元文件不需要执行权限
|
||||
err := os.WriteFile(systemdServiceFile, []byte(desc), 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = exec.Command(systemd, "stop", teaconst.SystemdServiceName+".service").Run()
|
||||
_ = exec.Command(systemd, "daemon-reload").Run()
|
||||
return exec.Command(systemd, "enable", teaconst.SystemdServiceName+".service").Run()
|
||||
// 停止已有服务
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, systemd, "stop", shortName+".service").Run()
|
||||
|
||||
// 重新加载
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, systemd, "daemon-reload").Run()
|
||||
|
||||
// 启用开机自启
|
||||
return executils.NewTimeoutCmd(10*time.Second, systemd, "enable", shortName+".service").Run()
|
||||
}
|
||||
|
||||
func (m *ServiceManager) Uninstall() error {
|
||||
if os.Getgid() != 0 {
|
||||
return errors.New("only root users can uninstall the service")
|
||||
}
|
||||
// installInitService 安装 init.d 服务(降级方案,适用于无 systemd 的旧系统)
|
||||
func (m *ServiceManager) installInitService(exePath string, args []string) error {
|
||||
shortName := teaconst.SystemdServiceName
|
||||
longName := "GoEdge HTTPDNS"
|
||||
|
||||
systemd, err := exec.LookPath("systemctl")
|
||||
// 生成 init.d 脚本
|
||||
script := `#!/bin/bash
|
||||
### BEGIN INIT INFO
|
||||
# Provides: ` + shortName + `
|
||||
# Required-Start: $all
|
||||
# Required-Stop:
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: ` + longName + ` Service
|
||||
### END INIT INFO
|
||||
|
||||
INSTALL_DIR=` + Tea.Root + `
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
cd "${INSTALL_DIR}"
|
||||
` + exePath + ` daemon &
|
||||
echo "` + longName + ` started"
|
||||
;;
|
||||
stop)
|
||||
cd "${INSTALL_DIR}"
|
||||
` + exePath + ` stop
|
||||
echo "` + longName + ` stopped"
|
||||
;;
|
||||
restart)
|
||||
cd "${INSTALL_DIR}"
|
||||
` + exePath + ` stop
|
||||
sleep 1
|
||||
` + exePath + ` daemon &
|
||||
echo "` + longName + ` restarted"
|
||||
;;
|
||||
status)
|
||||
cd "${INSTALL_DIR}"
|
||||
` + exePath + ` status
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart|status}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
`
|
||||
|
||||
// init.d 脚本需要执行权限
|
||||
err := os.WriteFile(initServiceFile, []byte(script), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = exec.Command(systemd, "disable", teaconst.SystemdServiceName+".service").Run()
|
||||
_ = exec.Command(systemd, "daemon-reload").Run()
|
||||
return os.Remove(systemdServiceFile)
|
||||
// 尝试用 chkconfig 注册(CentOS/RHEL)
|
||||
chkCmd, err := executils.LookPath("chkconfig")
|
||||
if err == nil {
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, chkCmd, "--add", teaconst.ProcessName).Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 尝试用 update-rc.d 注册(Debian/Ubuntu)
|
||||
updateRcCmd, err := executils.LookPath("update-rc.d")
|
||||
if err == nil {
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, updateRcCmd, teaconst.ProcessName, "defaults").Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fileExists 检查文件是否存在
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user