package router import ( "bytes" "errors" "fmt" "os" "os/exec" "os/signal" "strings" "syscall" "text/template" "github.com/kardianos/service" "github.com/Control-D-Inc/ctrld/internal/router/nvram" ) type ddwrtSvc struct { i service.Interface platform string *service.Config rcStartup string } func newddwrtService(i service.Interface, platform string, c *service.Config) (service.Service, error) { s := &ddwrtSvc{ i: i, platform: platform, Config: c, } if err := os.MkdirAll("/jffs/etc/config", 0644); err != nil { return nil, err } return s, nil } func (s *ddwrtSvc) String() string { if len(s.DisplayName) > 0 { return s.DisplayName } return s.Name } func (s *ddwrtSvc) Platform() string { return s.platform } func (s *ddwrtSvc) configPath() string { return fmt.Sprintf("/jffs/etc/config/%s.startup", s.Config.Name) } func (s *ddwrtSvc) template() *template.Template { return template.Must(template.New("").Parse(ddwrtSvcScript)) } func (s *ddwrtSvc) Install() error { confPath := s.configPath() if _, err := os.Stat(confPath); err == nil { return fmt.Errorf("already installed: %s", confPath) } path, err := os.Executable() if err != nil { return err } if !strings.HasPrefix(path, "/jffs/") { return errors.New("could not install service outside /jffs") } var to = &struct { *service.Config Path string }{ s.Config, path, } f, err := os.Create(confPath) if err != nil { return err } defer f.Close() if err := s.template().Execute(f, to); err != nil { return err } if err = os.Chmod(confPath, 0755); err != nil { return err } var sb strings.Builder if err := template.Must(template.New("").Parse(ddwrtStartupCmd)).Execute(&sb, to); err != nil { return err } s.rcStartup = sb.String() curVal, err := nvram.Run("get", nvram.RCStartupKey) if err != nil { return err } if _, err := nvram.Run("set", nvram.CtrldKeyPrefix+nvram.RCStartupKey+"="+curVal); err != nil { return err } val := strings.Join([]string{curVal, s.rcStartup + " &", fmt.Sprintf(`echo $! > "/tmp/%s.pid"`, s.Config.Name)}, "\n") if _, err := nvram.Run("set", nvram.RCStartupKey+"="+val); err != nil { return err } if out, err := nvram.Run("commit"); err != nil { return fmt.Errorf("%s: %w", out, err) } return nil } func (s *ddwrtSvc) Uninstall() error { if err := os.Remove(s.configPath()); err != nil { return err } ctrldStartupKey := nvram.CtrldKeyPrefix + nvram.RCStartupKey rcStartup, err := nvram.Run("get", ctrldStartupKey) if err != nil { return err } _, _ = nvram.Run("unset", ctrldStartupKey) if _, err := nvram.Run("set", nvram.RCStartupKey+"="+rcStartup); err != nil { return err } if out, err := nvram.Run("commit"); err != nil { return fmt.Errorf("%s: %w", out, err) } return nil } func (s *ddwrtSvc) Logger(errs chan<- error) (service.Logger, error) { if service.Interactive() { return service.ConsoleLogger, nil } return s.SystemLogger(errs) } func (s *ddwrtSvc) SystemLogger(errs chan<- error) (service.Logger, error) { // TODO(cuonglm): detect syslog enable and return proper logger? // this at least works with default configuration. if service.Interactive() { return service.ConsoleLogger, nil } return &noopLogger{}, nil } func (s *ddwrtSvc) 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 *ddwrtSvc) 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 *ddwrtSvc) Start() error { return exec.Command(s.configPath(), "start").Run() } func (s *ddwrtSvc) Stop() error { return exec.Command(s.configPath(), "stop").Run() } func (s *ddwrtSvc) Restart() error { err := s.Stop() if err != nil { return err } return s.Start() } type noopLogger struct { } func (c noopLogger) Error(v ...interface{}) error { return nil } func (c noopLogger) Warning(v ...interface{}) error { return nil } func (c noopLogger) Info(v ...interface{}) error { return nil } func (c noopLogger) Errorf(format string, a ...interface{}) error { return nil } func (c noopLogger) Warningf(format string, a ...interface{}) error { return nil } func (c noopLogger) Infof(format string, a ...interface{}) error { return nil } const ddwrtStartupCmd = `{{.Path}}{{range .Arguments}} {{.}}{{end}}` const ddwrtSvcScript = `#!/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 echo "Already started" else echo "Starting $name" $cmd & echo $! > "$pid_file" chmod 600 "$pid_file" if ! is_running; then echo "Failed to start $name" exit 1 fi fi ;; stop) if is_running; then echo -n "Stopping $name..." kill "$(get_pid)" for _ in 1 2 3 4 5; do if ! is_running; then echo "stopped" if [ -f "$pid_file" ]; then rm "$pid_file" fi exit 0 fi printf "." sleep 2 done echo "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 ;; *) echo "Usage: $0 {start|stop|restart|status}" exit 1 ;; esac exit 0 `