zephinzer/godev

View on GitHub
command.go

Summary

Maintainability
A
35 mins
Test Coverage
package main
 
import (
"crypto/md5"
"errors"
"fmt"
"os"
"os/exec"
"path"
"strings"
"syscall"
)
 
// CommandDelimiter is used when demarcating boundaries between
// functions
const CommandDelimiter = "───────────────────────────────────────────"
 
// CommandProcessStartSymbol is the fancy symbol we use to denote
// the start of a command
const CommandProcessStartSymbol = "â–º"
 
// CommandProcessStopSymbol is the fancy symbol we use to denote
// the end of a command
const CommandProcessStopSymbol = "â– "
 
// ICommand is the interface for the Command class
type ICommand interface {
// runs the command
Run()
// gets the id of the command
GetID() string
// get a pointer to the status channel
GetStatus() *chan error
// checks if the command is still running
IsRunning() bool
// checks if the command is valid
IsValid() error
// tells command to exit nicely
SendInterrupt()
}
 
// InitCommand is for creating a new Command
func InitCommand(config *CommandConfig) *Command {
commandHash := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s%v", config.Application, config.Arguments))))
command := &Command{
id: commandHash[:6],
}
command.config = config
command.logger = InitLogger(&LoggerConfig{
Name: "command",
Format: "production",
Level: config.LogLevel,
AdditionalFields: &map[string]interface{}{
"submodule": path.Base(fmt.Sprintf("%s", config.Application)),
},
})
return command
}
 
// CommandConfig configures Command
type CommandConfig struct {
Application string
Arguments []string
Directory string
Environment []string
LogLevel LogLevel
}
 
// Command is the atomic command to run
type Command struct {
id string
signal chan os.Signal
status chan error
run chan error
terminated chan error
config *CommandConfig
cmd *exec.Cmd
logger *Logger
started bool
reported bool
stopped bool
}
 
// GetID returns the command's ID, used for the execution group
// to report the running command
func (command *Command) GetID() string {
return command.id
}
 
// GetStatus returns the command's status channel for the execution
// group to know when the command has terminated
func (command *Command) GetStatus() *chan error {
return &command.status
}
 
// IsRunning allows callers to check if the command is running,
// the logic is tied into the Run()
func (command *Command) IsRunning() bool {
return command.started && !command.stopped
}
 
// IsValid does some sanity checks on the provided
// application before we try to run it
Method `Command.IsValid` has 5 return statements (exceeds 4 allowed).
func (command *Command) IsValid() error {
application := command.config.Application
if len(application) == 0 {
return errors.New("no application was specified")
}
if path.IsAbs(application) {
if _, err := os.Lstat(application); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("application at '%s' could not be found", application)
}
return err
}
}
if _, err := exec.LookPath(application); err != nil {
return err
}
return nil
}
 
// Run executes the command
func (command *Command) Run() {
command.logger.Tracef("command[%s] is starting", command.id)
command.handleInitialisation()
go command.handleStart()
go command.handleProcessLifecycle()
select {
case terminateCommand := <-command.terminated:
command.handleStopped(terminateCommand)
}
}
 
// SendInterrupt sends SIGINT to the command
func (command *Command) SendInterrupt() {
command.logger.Tracef("SIGINT received by command %s", command.id)
command.logger.Tracef("command[%v] status: %v/%v, msg: SIGINT >>> %v", command.id, command.started, command.terminated, &command.signal)
command.signal <- syscall.SIGINT
}
 
func (command *Command) handleInitialisation() {
if command.config == nil {
panic("command.config needs to be defined before initialisation can be done")
}
command.signal = make(chan os.Signal, 0)
command.status = make(chan error, 0)
command.run = make(chan error, 0)
command.terminated = make(chan error, 0)
command.started = false
command.reported = false
command.stopped = false
command.cmd = exec.Command(
command.config.Application,
command.config.Arguments...,
)
command.cmd.Dir = command.config.Directory
command.cmd.Env = command.config.Environment
for _, envvar := range os.Environ() {
command.cmd.Env = append(command.cmd.Env, envvar)
}
// command.cmd.Env = append(command.config.Environment, "GOCACHE=on")
command.cmd.Stderr = os.Stderr
command.cmd.Stdout = os.Stdout
}
 
// handleProcessExited handles the exit status being sent by the process
func (command *Command) handleProcessExited(status error) error {
command.logger.Tracef("process status: %v", command.cmd.ProcessState)
command.terminated <- status
return nil
}
 
func (command *Command) handleProcessLifecycle() error {
for {
select {
case signal := <-command.signal: // caller -> Command: shut down please
return command.handleSignalReceived(signal)
case cmdRunStatus := <-command.run: // process -> Command: i'm done here
return command.handleProcessExited(cmdRunStatus)
default: // just run
command.handleProcessReporting()
}
}
}
 
// handleProcessReporting handles the CLI reporting after a process
// has its PID reported
func (command *Command) handleProcessReporting() {
if !command.reported {
if command.cmd.Process != nil {
command.logger.Infof(
"'%v'\n%s %s pid:%v id:%s %s",
strings.Join(command.config.Arguments, "', '"),
CommandProcessStartSymbol,
CommandDelimiter,
command.cmd.Process.Pid,
command.id,
CommandProcessStartSymbol,
)
command.reported = true
}
}
}
 
// handleSignalReceived handles the signal received by the caller
func (command *Command) handleSignalReceived(signal os.Signal) error {
command.logger.Tracef("caller sent signal %v", signal)
command.terminated <- errors.New(signal.String())
if err := command.cmd.Process.Signal(signal); err != nil {
command.logger.Warn(err)
return err
}
return nil
}
 
// handleStart starts the process
func (command *Command) handleStart() {
command.started = true
command.run <- command.cmd.Run()
}
 
// handleStopped processes the end of a command as reported
// by (*exec.Cmd).Run or (*exec.Cmd).Wait
func (command *Command) handleStopped(terminateCommand error) {
command.logger.Tracef("command[%s] is exiting (%v)", command.id, terminateCommand)
pid := -1
if command.cmd.Process != nil {
pid = command.cmd.Process.Pid
}
command.logger.Infof(
"\n%s %s pid:%v id:%s %s",
CommandProcessStopSymbol,
CommandDelimiter,
pid,
command.id,
CommandProcessStopSymbol,
)
command.stopped = true
command.status <- terminateCommand
}