package webcmd import ( "bufio" "context" "crypto/rand" "encoding/base32" "errors" "io" "os/exec" "sync" "time" ) type OutputLine struct { isError bool text string } type Task struct { cmd *exec.Cmd output []OutputLine started int64 stopped int64 exitCode int cancel context.CancelFunc } func (t Task) Finished() bool { return t.stopped != 0 } func uuid() string { buff := make([]byte, 15) // multiples of five are best for base32 _, err := rand.Read(buff) if err != nil { panic(err) } return base32.StdEncoding.EncodeToString(buff) } // LaunchTask creates a new task from the given command parameters. func (this *App) LaunchTask(workDir string, params []string) (taskRef string, err error) { if len(params) == 0 { return "", errors.New("No parameters for task") } ref := uuid() ctx, cancel := context.WithCancel(context.Background()) cmd := exec.CommandContext(ctx, params[0], params[1:]...) errPipe, err := cmd.StderrPipe() if err != nil { return "", err } outPipe, err := cmd.StdoutPipe() if err != nil { return "", err } cmd.Dir = workDir err = cmd.Start() if err != nil { return "", err } this.tasksMtx.Lock() this.tasks[ref] = Task{ cmd: cmd, output: make([]OutputLine, 0), started: time.Now().Unix(), stopped: 0, exitCode: 0, cancel: cancel, } this.tasksMtx.Unlock() wg := sync.WaitGroup{} wg.Add(2) writeline := func(text string, isError bool) { this.tasksMtx.Lock() defer this.tasksMtx.Unlock() task := this.tasks[ref] task.output = append(task.output, OutputLine{isError: isError, text: text}) if this.cfg.MaxHistoryLines > 0 && len(task.output) > 2*this.cfg.MaxHistoryLines { task.output = task.output[this.cfg.MaxHistoryLines:] } this.tasks[ref] = task } pipe2line := func(rc io.ReadCloser, isError bool) { defer wg.Done() sc := bufio.NewScanner(rc) for sc.Scan() { writeline(sc.Text(), isError) } rc.Close() } go pipe2line(errPipe, true) go pipe2line(outPipe, false) go func() { wg.Wait() err := cmd.Wait() stopTime := time.Now().Unix() exitCode := 0 if err != nil { writeline(err.Error(), true) exitCode = 1 } this.tasksMtx.Lock() defer this.tasksMtx.Unlock() task := this.tasks[ref] task.stopped = stopTime task.exitCode = exitCode this.tasks[ref] = task }() return ref, nil }