~craftyguy/superd

a8136c3c9aa7f034a9a00a4bbad5f9be5ce3f566 — Clayton Craft 1 year, 10 months ago f72a0d5
service: send stdout/stderr to a log file

for #38
1 files changed, 53 insertions(+), 7 deletions(-)

M internal/service/service.go
M internal/service/service.go => internal/service/service.go +53 -7
@@ 7,6 7,7 @@ import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"


@@ 16,6 17,7 @@ import (
	"sr.ht/~craftyguy/superd/internal/config"
	"sr.ht/~craftyguy/superd/internal/env"
	"sr.ht/~craftyguy/superd/internal/log"
	"sr.ht/~craftyguy/superd/internal/misc"
	"sr.ht/~craftyguy/superd/pkg/api"
)



@@ 181,7 183,7 @@ func (s *Service) runCmd(c cmd.Commander) (err error) {
// startup service routine, e.g. running ExecStartPre, ExecStart. Returns
// channels for receiving the command object if successful or service status on
// failure. the cancel chan is used to signal to the routine to abort startup (e.g. when retrying)
func (s *Service) startup(cancel <-chan struct{}) (cmdChan chan cmd.Commander, startStatus chan api.ServiceStatus) {
func (s *Service) startup(cancel <-chan struct{}, logFile *os.File) (cmdChan chan cmd.Commander, startStatus chan api.ServiceStatus) {
	startStatus = make(chan api.ServiceStatus)
	cmdChan = make(chan cmd.Commander)



@@ 193,7 195,7 @@ func (s *Service) startup(cancel <-chan struct{}) (cmdChan chan cmd.Commander, s

		// ExecStartPre
		if conf.ExecStartPreCmd != "" {
			preCmd := s.newCommand(conf.ExecStartPreCmd, conf.ExecStartPreCmdArgs, nil)
			preCmd := s.newCommand(conf.ExecStartPreCmd, conf.ExecStartPreCmdArgs, logFile)
			if err := s.runCmd(preCmd); err != nil && !errors.As(err, &cmd.ErrCmdExited{}) {
				startStatus <- api.ServiceFailed
				return


@@ 223,7 225,7 @@ func (s *Service) startup(cancel <-chan struct{}) (cmdChan chan cmd.Commander, s
				tries = 0
			}

			startCmd = s.newCommand(conf.Cmd, conf.CmdArgs, nil)
			startCmd = s.newCommand(conf.Cmd, conf.CmdArgs, logFile)
			if err := s.runCmd(startCmd); err != nil {
				switch err.(type) {
				case cmd.ErrCmdExitedNonZero:


@@ 257,7 259,7 @@ func (s *Service) startup(cancel <-chan struct{}) (cmdChan chan cmd.Commander, s

		// ExecStartPost
		if conf.ExecStartPostCmd != "" {
			postCmd := s.newCommand(conf.ExecStartPostCmd, conf.ExecStartPostCmdArgs, nil)
			postCmd := s.newCommand(conf.ExecStartPostCmd, conf.ExecStartPostCmdArgs, logFile)
			if err := s.runCmd(postCmd); err != nil && !errors.As(err, &cmd.ErrCmdExited{}) {
				startStatus <- api.ServiceFailed
				return


@@ 281,6 283,34 @@ func (s *Service) Uptime() int64 {
// make killTimeout overridable in testing
var killTimeout = time.Duration(5 * time.Second)

func (s *Service) newLogFile() (file *os.File, err error) {
	logDir, ok := os.LookupEnv("XDG_STATE_HOME")
	if !ok {
		logDir = "~/.local/state"
	}
	logDir = filepath.Join(misc.ExpandHome(logDir), "superd", "logs")
	logFilePath := filepath.Join(logDir, s.Name()+".log")

	if err = os.MkdirAll(logDir, 0750); err != nil {
		err = fmt.Errorf("unable to create dir %q for service log file: %w", logDir, err)
		return
	}

	// rotate any old log file to <name>.old
	// Note: Error is ignored since this may fail if the log doesn't exist. For
	// all other types of failures, we'll fail later when creating or writing
	// to it...
	_ = os.Rename(logFilePath, logFilePath+".old")

	file, err = os.Create(logFilePath)
	if err != nil {
		err = fmt.Errorf("unable to create service log file %q: %w", logFilePath, err)
		return
	}

	return
}

func (s *Service) Run(quit <-chan struct{}) {

	var (


@@ 302,8 332,9 @@ func (s *Service) Run(quit <-chan struct{}) {

		// command streams
		cmdStatus <-chan goCmd.Status
		cmdStdout <-chan string
		cmdStderr <-chan string

		//  for stdout/stderr
		logFile *os.File
	)
	details.Status = api.ServiceStopped



@@ 313,10 344,19 @@ func (s *Service) Run(quit <-chan struct{}) {
		// Start (process) requested
		case <-s.Start:
			if details.Status != api.ServiceStarted && details.Status != api.ServiceStarting {

				// declare err, since using := results in a new logFile being
				// instantiated in this scope
				var err error
				logFile, err = s.newLogFile()
				if err != nil {
					s.logger.Errorln(err.Error())
				}

				shouldRestart = true
				shouldStop = false
				cancelStartChan = make(chan struct{})
				cmdChan, startStatus = s.startup(cancelStartChan)
				cmdChan, startStatus = s.startup(cancelStartChan, logFile)
				details.Status = api.ServiceStarting
			}



@@ 412,6 452,12 @@ func (s *Service) Run(quit <-chan struct{}) {
				s.logger.Debugln(s.Name(), "exited with: "+fmt.Sprint(status.Exit))
				s.getCmdStatus = nil

				if logFile != nil {
					if err := logFile.Close(); err != nil {
						s.logger.Errorf("unable to close service log file: %s", err.Error())
					}
				}

				failed := false
				if status.Error != nil {
					failed = true