mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
For safety reason, ctrld will create a backup of the current binary when running upgrade command. However, on systems where ctrld status is got by parsing ps command output, the current binary path is important and must be the same with the original binary. Depends on kernel version, using os.Executable may return new backup binary path, aka "ctrld_previous", not the original "ctrld" binary. This causes upgrade command see ctrld as not running after restart -> upgrade failed. Fixing this by recording the binary path before creating new service, so the ctrld service status can be checked correctly.
361 lines
7.6 KiB
Go
361 lines
7.6 KiB
Go
package router
|
||
|
||
import (
|
||
"bytes"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"os/exec"
|
||
"os/signal"
|
||
"path/filepath"
|
||
"strings"
|
||
"syscall"
|
||
"text/template"
|
||
|
||
"github.com/kardianos/service"
|
||
|
||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
||
)
|
||
|
||
const (
|
||
merlinJFFSScriptPath = "/jffs/scripts/services-start"
|
||
merlinJFFSServiceEventScriptPath = "/jffs/scripts/service-event"
|
||
)
|
||
|
||
type merlinSvc struct {
|
||
i service.Interface
|
||
platform string
|
||
*service.Config
|
||
}
|
||
|
||
func newMerlinService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
|
||
s := &merlinSvc{
|
||
i: i,
|
||
platform: platform,
|
||
Config: c,
|
||
}
|
||
return s, nil
|
||
}
|
||
|
||
func (s *merlinSvc) String() string {
|
||
if len(s.DisplayName) > 0 {
|
||
return s.DisplayName
|
||
}
|
||
return s.Name
|
||
}
|
||
|
||
func (s *merlinSvc) Platform() string {
|
||
return s.platform
|
||
}
|
||
|
||
func (s *merlinSvc) configPath() string {
|
||
bin := s.Config.Executable
|
||
if bin == "" {
|
||
path, err := os.Executable()
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
bin = path
|
||
}
|
||
return bin + ".startup"
|
||
}
|
||
|
||
func (s *merlinSvc) template() *template.Template {
|
||
return template.Must(template.New("").Parse(merlinSvcScript))
|
||
}
|
||
|
||
func (s *merlinSvc) Install() error {
|
||
exePath, err := os.Executable()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if !strings.HasPrefix(exePath, "/jffs/") {
|
||
return errors.New("could not install service outside /jffs")
|
||
}
|
||
if _, err := nvram.Run("set", "jffs2_scripts=1"); err != nil {
|
||
return err
|
||
}
|
||
if _, err := nvram.Run("commit"); err != nil {
|
||
return err
|
||
}
|
||
|
||
confPath := s.configPath()
|
||
if _, err := os.Stat(confPath); err == nil {
|
||
return fmt.Errorf("already installed: %s", confPath)
|
||
}
|
||
|
||
var to = &struct {
|
||
*service.Config
|
||
Path string
|
||
}{
|
||
s.Config,
|
||
exePath,
|
||
}
|
||
|
||
f, err := os.Create(confPath)
|
||
if err != nil {
|
||
return fmt.Errorf("os.Create: %w", err)
|
||
}
|
||
defer f.Close()
|
||
|
||
if err := s.template().Execute(f, to); err != nil {
|
||
return fmt.Errorf("s.template.Execute: %w", err)
|
||
}
|
||
|
||
if err = os.Chmod(confPath, 0755); err != nil {
|
||
return fmt.Errorf("os.Chmod: startup script: %w", err)
|
||
}
|
||
|
||
if err := os.MkdirAll(filepath.Dir(merlinJFFSScriptPath), 0755); err != nil {
|
||
return fmt.Errorf("os.MkdirAll: %w", err)
|
||
}
|
||
|
||
tmpScript, err := os.CreateTemp("", "ctrld_install")
|
||
if err != nil {
|
||
return fmt.Errorf("os.CreateTemp: %w", err)
|
||
}
|
||
defer os.Remove(tmpScript.Name())
|
||
defer tmpScript.Close()
|
||
|
||
if _, err := tmpScript.WriteString(merlinAddLineToScript); err != nil {
|
||
return fmt.Errorf("tmpScript.WriteString: %w", err)
|
||
}
|
||
if err := tmpScript.Close(); err != nil {
|
||
return fmt.Errorf("tmpScript.Close: %w", err)
|
||
}
|
||
addLineToScript := func(line, script string) error {
|
||
if _, err := os.Stat(script); os.IsNotExist(err) {
|
||
if err := os.WriteFile(script, []byte("#!/bin/sh\n"), 0755); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if err := os.Chmod(script, 0755); err != nil {
|
||
return fmt.Errorf("os.Chmod: jffs script: %w", err)
|
||
}
|
||
|
||
if err := exec.Command("sh", tmpScript.Name(), line, script).Run(); err != nil {
|
||
return fmt.Errorf("exec.Command: add startup script: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
for script, line := range map[string]string{
|
||
merlinJFFSScriptPath: s.configPath() + " start",
|
||
merlinJFFSServiceEventScriptPath: s.configPath() + ` service_event "$1" "$2"`,
|
||
} {
|
||
if err := addLineToScript(line, script); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (s *merlinSvc) Uninstall() error {
|
||
if err := os.Remove(s.configPath()); err != nil {
|
||
return fmt.Errorf("os.Remove: %w", err)
|
||
}
|
||
tmpScript, err := os.CreateTemp("", "ctrld_uninstall")
|
||
if err != nil {
|
||
return fmt.Errorf("os.CreateTemp: %w", err)
|
||
}
|
||
defer os.Remove(tmpScript.Name())
|
||
defer tmpScript.Close()
|
||
|
||
if _, err := tmpScript.WriteString(merlinRemoveLineFromScript); err != nil {
|
||
return fmt.Errorf("tmpScript.WriteString: %w", err)
|
||
}
|
||
if err := tmpScript.Close(); err != nil {
|
||
return fmt.Errorf("tmpScript.Close: %w", err)
|
||
}
|
||
removeLineFromScript := func(line, script string) error {
|
||
if _, err := os.Stat(script); os.IsNotExist(err) {
|
||
if err := os.WriteFile(script, []byte("#!/bin/sh\n"), 0755); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if err := os.Chmod(script, 0755); err != nil {
|
||
return fmt.Errorf("os.Chmod: jffs script: %w", err)
|
||
}
|
||
|
||
if err := exec.Command("sh", tmpScript.Name(), line, script).Run(); err != nil {
|
||
return fmt.Errorf("exec.Command: add startup script: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
for script, line := range map[string]string{
|
||
merlinJFFSScriptPath: s.configPath() + " start",
|
||
merlinJFFSServiceEventScriptPath: s.configPath() + ` service_event "$1" "$2"`,
|
||
} {
|
||
if err := removeLineFromScript(line, script); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (s *merlinSvc) Logger(errs chan<- error) (service.Logger, error) {
|
||
if service.Interactive() {
|
||
return service.ConsoleLogger, nil
|
||
}
|
||
return s.SystemLogger(errs)
|
||
}
|
||
|
||
func (s *merlinSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
|
||
return newSysLogger(s.Name, errs)
|
||
}
|
||
|
||
func (s *merlinSvc) Run() (err error) {
|
||
err = s.i.Start(s)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if interactice, _ := isInteractive(); !interactice {
|
||
signal.Ignore(syscall.SIGHUP)
|
||
}
|
||
|
||
var sigChan = make(chan os.Signal, 1)
|
||
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
||
<-sigChan
|
||
|
||
return s.i.Stop(s)
|
||
}
|
||
|
||
func (s *merlinSvc) Status() (service.Status, error) {
|
||
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
|
||
return service.StatusUnknown, service.ErrNotInstalled
|
||
}
|
||
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
|
||
if err != nil {
|
||
return service.StatusUnknown, err
|
||
}
|
||
switch string(bytes.TrimSpace(out)) {
|
||
case "running":
|
||
return service.StatusRunning, nil
|
||
default:
|
||
return service.StatusStopped, nil
|
||
}
|
||
}
|
||
|
||
func (s *merlinSvc) Start() error {
|
||
return exec.Command(s.configPath(), "start").Run()
|
||
}
|
||
|
||
func (s *merlinSvc) Stop() error {
|
||
return exec.Command(s.configPath(), "stop").Run()
|
||
}
|
||
|
||
func (s *merlinSvc) Restart() error {
|
||
err := s.Stop()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return s.Start()
|
||
}
|
||
|
||
const merlinSvcScript = `#!/bin/sh
|
||
|
||
name="{{.Name}}"
|
||
cmd="{{.Path}}{{range .Arguments}} {{.}}{{end}}"
|
||
pid_file="/tmp/$name.pid"
|
||
|
||
get_pid() {
|
||
cat "$pid_file"
|
||
}
|
||
|
||
is_running() {
|
||
[ -f "$pid_file" ] && ps | grep -q "^ *$(get_pid) "
|
||
}
|
||
|
||
case "$1" in
|
||
start)
|
||
if is_running; then
|
||
logger -c "Already started"
|
||
else
|
||
logger -c "Starting $name"
|
||
if [ -f /rom/ca-bundle.crt ]; then
|
||
# For John’s fork
|
||
export SSL_CERT_FILE=/rom/ca-bundle.crt
|
||
fi
|
||
$cmd &
|
||
echo $! > "$pid_file"
|
||
chmod 600 "$pid_file"
|
||
if ! is_running; then
|
||
logger -c "Failed to start $name"
|
||
exit 1
|
||
fi
|
||
fi
|
||
;;
|
||
stop)
|
||
if is_running; then
|
||
logger -c "Stopping $name..."
|
||
kill "$(get_pid)"
|
||
for _ in 1 2 3 4 5; do
|
||
if ! is_running; then
|
||
logger -c "stopped"
|
||
if [ -f "$pid_file" ]; then
|
||
rm "$pid_file"
|
||
fi
|
||
exit 0
|
||
fi
|
||
printf "."
|
||
sleep 2
|
||
done
|
||
logger -c "failed to stop $name"
|
||
exit 1
|
||
fi
|
||
exit 0
|
||
;;
|
||
restart)
|
||
$0 stop
|
||
$0 start
|
||
;;
|
||
status)
|
||
if is_running; then
|
||
echo "running"
|
||
else
|
||
echo "stopped"
|
||
exit 1
|
||
fi
|
||
;;
|
||
service_event)
|
||
event=$2
|
||
svc=$3
|
||
dnsmasq_pid_file=$(sed -n '/pid-file=/s///p' /etc/dnsmasq.conf)
|
||
|
||
if [ "$event" = "restart" ] && [ "$svc" = "diskmon" ]; then
|
||
kill "$(cat "$dnsmasq_pid_file")" >/dev/null 2>&1
|
||
fi
|
||
;;
|
||
*)
|
||
echo "Usage: $0 {start|stop|restart|status}"
|
||
exit 1
|
||
;;
|
||
esac
|
||
exit 0
|
||
`
|
||
|
||
const merlinAddLineToScript = `#!/bin/sh
|
||
|
||
line=$1
|
||
file=$2
|
||
|
||
. /usr/sbin/helper.sh
|
||
|
||
pc_append "$line" "$file"
|
||
`
|
||
|
||
const merlinRemoveLineFromScript = `#!/bin/sh
|
||
|
||
line=$1
|
||
file=$2
|
||
|
||
. /usr/sbin/helper.sh
|
||
|
||
pc_delete "$line" "$file"
|
||
`
|