sarulabs/hydre

View on GitHub
daemon.go

Summary

Maintainability
A
0 mins
Test Coverage
package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "os"
    "os/exec"
    "strconv"
    "strings"
    "sync"
    "syscall"
    "time"

    "github.com/hpcloud/tail"
)

// Service is the interface used to mock the Daemon structure.
type Service interface {
    Start(stopNotifier chan<- string)
    Stop()
    Kill()
}

// Daemon is a program executed in the background.
// Command is the UNIX command that starts the Daemon.
// The program will be stopped with a SIGTERM signal,
// but if StopCommand is defined, it will be used to stop the Daemon instead.
// The program running in the background is Command except if PidFile is defined.
// In this case the program is the one that has the pid specified in the PidFile.
// If LogFiles is not empty, the files will be used to stream
// the logs of the Daemon in addition to the Command output.
// You should probably use LogFiles if PidFile is defined.
type Daemon struct {
    Name        string
    Command     []string `yaml:"command"`
    StopCommand []string `yaml:"stopCommand"`
    PidFile     string   `yaml:"pidFile"`
    LogFiles    []string `yaml:"logFiles"`
    mu          sync.Mutex
    process     *os.Process
}

// Start executes the Daemon Command.
// It sets the Daemon process that is either the command process
// or the process defined by the PidFile.
// It also streams the Daemon logs on the standard output.
// When the daemon dies or when its logs are not  streamed anymore,
// it sends a message in the stopNotifier channel
// to report that the Daemon is not working properly anymore.
func (d *Daemon) Start(stopNotifier chan<- string) {
    log.Printf("starting `%s` daemon with command : %s\n", d.Name, d.Command)

    cmd := exec.Command(d.Command[0], d.Command[1:]...)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    for _, logFile := range d.LogFiles {
        go d.printLogs(logFile, stopNotifier)
    }

    if d.PidFile == "" {
        d.startForeground(cmd, stopNotifier)
        return
    }

    d.startBackground(cmd, stopNotifier)
}

func (d *Daemon) startForeground(cmd *exec.Cmd, stopNotifier chan<- string) {
    if err := cmd.Start(); err != nil {
        stopNotifier <- fmt.Sprintf("could not start daemon `%s` : %s", d.Name, err)
        return
    }

    d.setProcess(cmd.Process)

    // send notification when the process dies
    go d.waitChild(stopNotifier)
}

func (d *Daemon) startBackground(cmd *exec.Cmd, stopNotifier chan<- string) {
    // remove pidfile content to be sure to read the correct value
    ioutil.WriteFile(d.PidFile, []byte{}, 0777)

    cmd.Run()

    pid := d.readPidFileRetry(500, 10*time.Millisecond)
    process, _ := os.FindProcess(pid)
    d.setProcess(process)

    // send notification when the process dies
    go d.wait(stopNotifier)
}

// waitChild waits for the Daemon process to finish and sends a message
// in the stopNotifier channel when that happens.
// The Daemon process need to be a child of this process.
func (d *Daemon) waitChild(stopNotifier chan<- string) {
    if p := d.getProcess(); p != nil {
        p.Wait()
    }

    stopNotifier <- fmt.Sprintf("daemon `%s` has stopped", d.Name)
}

// wait waits for the Daemon process to finish and sends a message
// in the stopNotifier channel when that happens.
// It checks every second that the process is still running.
func (d *Daemon) wait(stopNotifier chan<- string) {
    if p := d.getProcess(); p != nil {
        for isAlive(p.Pid) {
            time.Sleep(time.Second)
        }
    }

    stopNotifier <- fmt.Sprintf("daemon `%s` has stopped", d.Name)
}

// readPidFile returns the pid from the Daemon PidFile.
func (d *Daemon) readPidFile() (int, error) {
    b, err := ioutil.ReadFile(d.PidFile)
    if err != nil {
        return 0, err
    }

    pid := strings.Trim(string(b), " \n")

    return strconv.Atoi(pid)
}

// readPidFileRetry tries to read the pid from the Daemon PidFile.
// If it could not read it, it will retry maxAttempts times
// and will wait before each new attempt.
// If the pid file could not be red, it returns 0.
func (d *Daemon) readPidFileRetry(maxAttempts int, wait time.Duration) int {
    for i := 0; i < maxAttempts; i++ {
        if pid, _ := d.readPidFile(); pid != 0 {
            return pid
        }
    }

    return 0
}

// printLogs streams the content of a file on the standard output.
// When it stops working, it sends a message in the stopNotifier channel.
func (d *Daemon) printLogs(filename string, stopNotifier chan<- string) {
    t, _ := tail.TailFile(filename, tail.Config{Follow: true})

    for line := range t.Lines {
        fmt.Println(line.Text)
    }

    stopNotifier <- fmt.Sprintf("could not stream logs from `%s`", filename)
}

// Stop tries to gracefully stop the Daemon.
// If StopCommand is defined, it executes the command.
// If not it sends a SIGTERM signal to the Daemon process.
func (d *Daemon) Stop() {
    // there is a stop command to stop the process, execute it.
    if len(d.StopCommand) > 0 {
        cmd := exec.Command(d.StopCommand[0], d.StopCommand[1:]...)
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr
        cmd.Run()
        return
    }

    // there is no defined command to stop the process,
    // just send a SIGTERM signal.
    if p := d.getProcess(); len(d.StopCommand) == 0 && p != nil {
        p.Signal(syscall.SIGTERM)
        return
    }
}

// Kill sends a SIGKILL signal to the Daemon process.
func (d *Daemon) Kill() {
    if p := d.getProcess(); p != nil {
        p.Signal(syscall.SIGKILL)
    }
}

// getProcess is the thread safe process getter.
func (d *Daemon) getProcess() *os.Process {
    d.mu.Lock()
    defer d.mu.Unlock()
    return d.process
}

// getProcess is the thread safe process setter.
func (d *Daemon) setProcess(process *os.Process) {
    d.mu.Lock()
    defer d.mu.Unlock()
    d.process = process
}