grim/convey

closing merged branch
hostnames
2017-10-13, Gary Kramlich
33eae19fcbbe
closing merged branch
/*
* Convey
* Copyright 2016-2017 Gary Kramlich <grim@reaperworld.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package command
import (
"bufio"
"bytes"
"os/exec"
"strings"
"sync"
"text/template"
"time"
"github.com/kballard/go-shellquote"
"bitbucket.org/rw_grim/convey/logging"
"github.com/aphistic/gomol"
)
var (
lock sync.Mutex
)
func Run(name, cmdTemplate string, params map[string]interface{}, timeout time.Duration) error {
var (
logger = logging.NewAdapter(name)
outCollector = newLogCollector(logger, gomol.LevelInfo)
errCollector = newLogCollector(logger, gomol.LevelError)
)
return run(name, cmdTemplate, params, timeout, outCollector, errCollector)
}
func RunOutput(name, cmdTemplate string, params map[string]interface{}, timeout time.Duration) (string, string, error) {
var (
wg = &sync.WaitGroup{}
outCollector = newStringCollector(wg)
errCollector = newStringCollector(wg)
)
wg.Add(2)
err := run(name, cmdTemplate, params, timeout, outCollector.handler, errCollector.handler)
wg.Wait()
return outCollector.output, errCollector.output, err
}
func execTemplate(name, cmdTemplate string, params map[string]interface{}) ([]string, error) {
// we use multiline comments to make the code readable, which leaves
// embedding newlines, so remove those.
cleanTemplate := strings.Replace(cmdTemplate, "\n", "", -1)
// go's template stuff is *NOT* thread safe, so we need to lock around it
lock.Lock()
tmpl, err := template.New(name).Parse(cleanTemplate)
lock.Unlock()
if err != nil {
return nil, err
}
// now execute the template
cmd := new(bytes.Buffer)
err = tmpl.Execute(cmd, params)
if err != nil {
return nil, err
}
return shellquote.Split(cmd.String())
}
func run(name, cmdTemplate string, params map[string]interface{}, timeout time.Duration, outHandler, errHandler collector) error {
logger := logging.NewAdapter(name)
cmdv, err := execTemplate(name, cmdTemplate, params)
if err != nil {
return err
}
logger.Debugf("running command \"%v\"", cmdv)
cmd := exec.Command(cmdv[0], cmdv[1:]...)
// setup the stdout wrapper
outReader, err := cmd.StdoutPipe()
if err != nil {
return err
}
// setup the stderr wrapper
errReader, err := cmd.StderrPipe()
if err != nil {
return err
}
// Pass output to the handlers
go outHandler(bufio.NewScanner(outReader))
go errHandler(bufio.NewScanner(errReader))
// now run the command
err = cmd.Start()
if err != nil {
return err
}
var timer *time.Timer
if timeout != 0 {
timer = time.AfterFunc(timeout, func() {
// kill process, unblock the cmd.Wait() below
cmd.Process.Kill()
// log that the process timed out
logger.Errorf("command %v timed out after %v", cmdv, timeout)
})
}
err = cmd.Wait()
if timer != nil {
timer.Stop()
}
return err
}