mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
So upgrading don't have to be initiated manually, helping large deployments to upgrade to latest ctrld version easily.
269 lines
6.8 KiB
Go
269 lines
6.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
|
|
"github.com/coreos/go-systemd/v22/unit"
|
|
"github.com/kardianos/service"
|
|
|
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
|
"github.com/Control-D-Inc/ctrld/internal/router/openwrt"
|
|
)
|
|
|
|
// newService wraps service.New call to return service.Service
|
|
// wrapper which is suitable for the current platform.
|
|
func newService(i service.Interface, c *service.Config) (service.Service, error) {
|
|
s, err := service.New(i, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch {
|
|
case router.IsOldOpenwrt(), router.IsNetGearOrbi():
|
|
return &procd{sysV: &sysV{s}, svcConfig: c}, nil
|
|
case router.IsGLiNet():
|
|
return &sysV{s}, nil
|
|
case s.Platform() == "unix-systemv":
|
|
return &sysV{s}, nil
|
|
case s.Platform() == "linux-systemd":
|
|
return &systemd{s}, nil
|
|
case s.Platform() == "darwin-launchd":
|
|
return newLaunchd(s), nil
|
|
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// sysV wraps a service.Service, and provide start/stop/status command
|
|
// base on "/etc/init.d/<service_name>".
|
|
//
|
|
// Use this on system where "service" command is not available, like GL.iNET router.
|
|
type sysV struct {
|
|
service.Service
|
|
}
|
|
|
|
func (s *sysV) installed() bool {
|
|
fi, err := os.Stat("/etc/init.d/ctrld")
|
|
if err != nil {
|
|
return false
|
|
}
|
|
mode := fi.Mode()
|
|
return mode.IsRegular() && (mode&0111) != 0
|
|
}
|
|
|
|
func (s *sysV) Start() error {
|
|
if !s.installed() {
|
|
return service.ErrNotInstalled
|
|
}
|
|
_, err := exec.Command("/etc/init.d/ctrld", "start").CombinedOutput()
|
|
return err
|
|
}
|
|
|
|
func (s *sysV) Stop() error {
|
|
if !s.installed() {
|
|
return service.ErrNotInstalled
|
|
}
|
|
_, err := exec.Command("/etc/init.d/ctrld", "stop").CombinedOutput()
|
|
return err
|
|
}
|
|
|
|
func (s *sysV) Restart() error {
|
|
if !s.installed() {
|
|
return service.ErrNotInstalled
|
|
}
|
|
// We don't care about error returned by s.Stop,
|
|
// because the service may already be stopped.
|
|
_ = s.Stop()
|
|
return s.Start()
|
|
}
|
|
|
|
func (s *sysV) Status() (service.Status, error) {
|
|
if !s.installed() {
|
|
return service.StatusUnknown, service.ErrNotInstalled
|
|
}
|
|
return unixSystemVServiceStatus()
|
|
}
|
|
|
|
// procd wraps a service.Service, and provide start/stop command
|
|
// base on "/etc/init.d/<service_name>", status command base on parsing "ps" command output.
|
|
//
|
|
// Use this on system where "/etc/init.d/<service_name> status" command is not available,
|
|
// like old GL.iNET Opal router.
|
|
type procd struct {
|
|
*sysV
|
|
svcConfig *service.Config
|
|
}
|
|
|
|
func (s *procd) Status() (service.Status, error) {
|
|
if !s.installed() {
|
|
return service.StatusUnknown, service.ErrNotInstalled
|
|
}
|
|
bin := s.svcConfig.Executable
|
|
if bin == "" {
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return service.StatusUnknown, nil
|
|
}
|
|
bin = exe
|
|
}
|
|
|
|
// Looking for something like "/sbin/ctrld run ".
|
|
shellCmd := fmt.Sprintf("ps | grep -q %q", bin+" [r]un ")
|
|
if err := exec.Command("sh", "-c", shellCmd).Run(); err != nil {
|
|
return service.StatusStopped, nil
|
|
}
|
|
return service.StatusRunning, nil
|
|
}
|
|
|
|
// systemd wraps a service.Service, and provide status command to
|
|
// report the status correctly.
|
|
type systemd struct {
|
|
service.Service
|
|
}
|
|
|
|
func (s *systemd) Status() (service.Status, error) {
|
|
out, _ := exec.Command("systemctl", "status", "ctrld").CombinedOutput()
|
|
if bytes.Contains(out, []byte("/FAILURE)")) {
|
|
return service.StatusStopped, nil
|
|
}
|
|
return s.Service.Status()
|
|
}
|
|
|
|
func (s *systemd) Start() error {
|
|
const systemdUnitFile = "/etc/systemd/system/ctrld.service"
|
|
f, err := os.Open(systemdUnitFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
if opts, change := ensureSystemdKillMode(f); change {
|
|
mode := os.FileMode(0644)
|
|
buf, err := io.ReadAll(unit.Serialize(opts))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(systemdUnitFile, buf, mode); err != nil {
|
|
return err
|
|
}
|
|
if out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput(); err != nil {
|
|
return fmt.Errorf("systemctl daemon-reload failed: %w\n%s", err, string(out))
|
|
}
|
|
mainLog.Load().Debug().Msg("set KillMode=process successfully")
|
|
}
|
|
return s.Service.Start()
|
|
}
|
|
|
|
// ensureSystemdKillMode ensure systemd unit file is configured with KillMode=process.
|
|
// This is necessary for running self-upgrade flow.
|
|
func ensureSystemdKillMode(r io.Reader) (opts []*unit.UnitOption, change bool) {
|
|
opts, err := unit.DeserializeOptions(r)
|
|
if err != nil {
|
|
mainLog.Load().Error().Err(err).Msg("failed to deserialize options")
|
|
return
|
|
}
|
|
change = true
|
|
needKillModeOpt := true
|
|
killModeOpt := unit.NewUnitOption("Service", "KillMode", "process")
|
|
for _, opt := range opts {
|
|
if opt.Match(killModeOpt) {
|
|
needKillModeOpt = false
|
|
change = false
|
|
break
|
|
}
|
|
if opt.Section == killModeOpt.Section && opt.Name == killModeOpt.Name {
|
|
opt.Value = killModeOpt.Value
|
|
needKillModeOpt = false
|
|
break
|
|
}
|
|
}
|
|
if needKillModeOpt {
|
|
opts = append(opts, killModeOpt)
|
|
}
|
|
return opts, change
|
|
}
|
|
|
|
func newLaunchd(s service.Service) *launchd {
|
|
return &launchd{
|
|
Service: s,
|
|
statusErrMsg: "Permission denied",
|
|
}
|
|
}
|
|
|
|
// launchd wraps a service.Service, and provide status command to
|
|
// report the status correctly when not running as root on Darwin.
|
|
//
|
|
// TODO: remove this wrapper once https://github.com/kardianos/service/issues/400 fixed.
|
|
type launchd struct {
|
|
service.Service
|
|
statusErrMsg string
|
|
}
|
|
|
|
func (l *launchd) Status() (service.Status, error) {
|
|
if os.Geteuid() != 0 {
|
|
return service.StatusUnknown, errors.New(l.statusErrMsg)
|
|
}
|
|
return l.Service.Status()
|
|
}
|
|
|
|
type task struct {
|
|
f func() error
|
|
abortOnError bool
|
|
Name string
|
|
}
|
|
|
|
func doTasks(tasks []task) bool {
|
|
for _, task := range tasks {
|
|
mainLog.Load().Debug().Msgf("Running task %s", task.Name)
|
|
if err := task.f(); err != nil {
|
|
if task.abortOnError {
|
|
mainLog.Load().Error().Msgf("error running task %s: %v", task.Name, err)
|
|
return false
|
|
}
|
|
// if this is darwin stop command, dont print debug
|
|
// since launchctl complains on every start
|
|
if runtime.GOOS != "darwin" || task.Name != "Stop" {
|
|
mainLog.Load().Debug().Msgf("error running task %s: %v", task.Name, err)
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func checkHasElevatedPrivilege() {
|
|
ok, err := hasElevatedPrivilege()
|
|
if err != nil {
|
|
mainLog.Load().Error().Msgf("could not detect user privilege: %v", err)
|
|
return
|
|
}
|
|
if !ok {
|
|
mainLog.Load().Error().Msg("Please relaunch process with admin/root privilege.")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func unixSystemVServiceStatus() (service.Status, error) {
|
|
out, err := exec.Command("/etc/init.d/ctrld", "status").CombinedOutput()
|
|
if err != nil {
|
|
// Specific case for openwrt >= 24.10, it returns non-success code
|
|
// for above status command, which may not right.
|
|
if router.Name() == openwrt.Name {
|
|
if string(bytes.ToLower(bytes.TrimSpace(out))) == "inactive" {
|
|
return service.StatusStopped, nil
|
|
}
|
|
}
|
|
return service.StatusUnknown, nil
|
|
}
|
|
|
|
switch string(bytes.ToLower(bytes.TrimSpace(out))) {
|
|
case "running":
|
|
return service.StatusRunning, nil
|
|
default:
|
|
return service.StatusStopped, nil
|
|
}
|
|
}
|