intake/core/execute.go

302 lines
7.3 KiB
Go
Raw Normal View History

2025-01-17 21:49:23 +00:00
package core
import (
"bufio"
"context"
"encoding/json"
2025-01-30 07:14:00 +00:00
"fmt"
2025-01-17 21:49:23 +00:00
"io"
"log"
"os"
"os/exec"
"strings"
"time"
)
2025-02-10 06:59:04 +00:00
func readPipe(
f io.ReadCloser,
send chan []byte,
done chan int,
2025-02-05 21:21:31 +00:00
) {
2025-02-10 06:59:04 +00:00
defer func() {
done <- 0
}()
var data []byte
var err error
reader := bufio.NewReader(f)
for data, err = reader.ReadBytes('\n'); err == nil; data, err = reader.ReadBytes('\n') {
send <- data
}
if err != io.EOF {
log.Printf("error: failed to read pipe: %v", err)
}
// In case the last line has no newline
if len(data) > 0 {
2025-02-10 06:59:04 +00:00
send <- data
2025-01-17 21:49:23 +00:00
}
}
2025-02-10 16:22:19 +00:00
// Two-stage error item helper, one stage to capture the immutable parameters and a second
// to include the variable error and logs.
func makeMakeErrItem(source string, argv []string) func(err error, logs []string) (item Item) {
return func(err error, logs []string) (item Item) {
item.Source = "default"
item.Id = RandomHex(16)
item.Title = fmt.Sprintf("Failed to execute for %s", source)
log := "no logs"
if len(logs) > 0 {
log = strings.Join(logs, "\n")
}
item.Body = fmt.Sprintf("<p><i>executing:</i> %#v</p><p><i>error:</i> %s</p><pre>%s</pre>", argv, err.Error(), log)
return
}
}
2025-01-17 21:49:23 +00:00
func Execute(
2025-01-23 20:26:21 +00:00
source string,
2025-01-17 21:49:23 +00:00
argv []string,
env []string,
state []byte,
2025-01-17 21:49:23 +00:00
input string,
timeout time.Duration,
2025-02-05 21:21:31 +00:00
postProcess func(item Item) Item,
2025-02-10 06:59:04 +00:00
) (
items []Item,
newState []byte,
2025-02-10 16:22:19 +00:00
errItem Item,
2025-02-10 06:59:04 +00:00
err error,
) {
2025-01-30 07:51:06 +00:00
log.Printf("executing %v", argv)
2025-01-17 21:49:23 +00:00
2025-02-10 16:22:19 +00:00
makeErrorItem := makeMakeErrItem(source, argv)
var logs []string
2025-02-21 15:38:03 +00:00
addLog := func(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
log.Print(msg)
logs = append(logs, msg)
}
2025-02-10 16:22:19 +00:00
2025-01-29 15:39:00 +00:00
if source == "" {
2025-02-10 16:22:19 +00:00
err = fmt.Errorf("empty source")
errItem = makeErrorItem(err, logs)
return
}
if len(argv) == 0 {
err = fmt.Errorf("empty argv for %s", source)
errItem = makeErrorItem(err, logs)
return
2025-01-29 15:39:00 +00:00
}
2025-01-17 21:49:23 +00:00
stateFile, err := os.CreateTemp("", "intake_state_*")
if err != nil {
2025-02-10 16:22:19 +00:00
err = fmt.Errorf("failed to create temp state file: %v", err)
errItem = makeErrorItem(err, logs)
return
}
defer func() {
if err := os.Remove(stateFile.Name()); err != nil {
log.Printf("error: failed to delete %s", stateFile.Name())
}
}()
_, err = stateFile.Write(state)
if err != nil {
2025-02-10 16:22:19 +00:00
err = fmt.Errorf("failed to write state file: %v", err)
errItem = makeErrorItem(err, logs)
return
}
env = append(env, "STATE_PATH="+stateFile.Name())
2025-01-17 21:49:23 +00:00
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...)
cmd.Env = append(os.Environ(), env...)
cmd.WaitDelay = time.Second * 5
// Open pipes to the command
stdin, err := cmd.StdinPipe()
if err != nil {
2025-02-10 16:22:19 +00:00
err = fmt.Errorf("failed to open stdin pipe: %v", err)
errItem = makeErrorItem(err, logs)
return
2025-01-17 21:49:23 +00:00
}
stdout, err := cmd.StdoutPipe()
if err != nil {
2025-02-10 16:22:19 +00:00
err = fmt.Errorf("failed to open stdout pipe: %v", err)
errItem = makeErrorItem(err, logs)
return
2025-01-17 21:49:23 +00:00
}
stderr, err := cmd.StderrPipe()
if err != nil {
2025-02-10 16:22:19 +00:00
err = fmt.Errorf("failed to open stderr pipe: %v", err)
errItem = makeErrorItem(err, logs)
return
2025-01-17 21:49:23 +00:00
}
2025-02-10 06:59:04 +00:00
cout := make(chan []byte)
cerr := make(chan []byte)
coutDone := make(chan int)
cerrDone := make(chan int)
2025-01-17 21:49:23 +00:00
// Routines handling the process i/o
2025-02-10 06:59:04 +00:00
go readPipe(stdout, cout, coutDone)
go readPipe(stderr, cerr, cerrDone)
2025-01-17 21:49:23 +00:00
// Kick off the command
err = cmd.Start()
if err != nil {
2025-02-10 16:22:19 +00:00
err = fmt.Errorf("failed to start execution: %v", err)
errItem = makeErrorItem(err, logs)
return
2025-01-17 21:49:23 +00:00
}
2025-02-10 06:59:04 +00:00
// Write any input to stdin and close it
2025-02-10 16:22:19 +00:00
if _, err = io.WriteString(stdin, input); err != nil {
err = fmt.Errorf("failed to write to stdin: %v", err)
errItem = makeErrorItem(err, logs)
return
}
if err = stdin.Close(); err != nil {
err = fmt.Errorf("failed to close stdin: %v", err)
errItem = makeErrorItem(err, logs)
return
}
2025-02-10 06:59:04 +00:00
// Collect outputs until std{out,err} close
parseError := false
stdoutDone := false
stderrDone := false
2025-02-21 05:03:39 +00:00
duplicateItem := ""
itemIds := make(map[string]bool)
2025-02-10 06:59:04 +00:00
monitor:
for {
select {
case data := <-cout:
var item Item
err := json.Unmarshal(data, &item)
if err != nil || item.Id == "" {
2025-02-21 15:38:03 +00:00
if err != nil {
addLog("[%s: item] parse error: %v", source, err)
}
addLog("[%s: stdout] %s", source, strings.TrimSpace(string(data)))
2025-02-10 06:59:04 +00:00
parseError = true
} else {
2025-02-21 05:03:39 +00:00
if itemIds[item.Id] {
2025-02-21 15:38:03 +00:00
addLog("[%s: item] %s (duplicate)", source, item.Id)
2025-02-21 05:03:39 +00:00
duplicateItem = item.Id
cmd.Cancel()
break monitor
}
itemIds[item.Id] = true
2025-02-10 06:59:04 +00:00
if postProcess != nil {
item = postProcess(item)
}
item.Active = true // These fields aren't up to
item.Created = 0 // the action to set and
item.Source = source // shouldn't be overrideable
2025-02-21 15:38:03 +00:00
addLog("[%s: item] %s", source, item.Id)
2025-02-10 06:59:04 +00:00
items = append(items, item)
}
case data := <-cerr:
2025-02-21 15:38:03 +00:00
addLog("[%s: stderr] %s", source, strings.TrimSpace(string(data)))
2025-02-10 06:59:04 +00:00
case <-coutDone:
stdoutDone = true
if stdoutDone && stderrDone {
break monitor
}
case <-cerrDone:
stderrDone = true
if stdoutDone && stderrDone {
break monitor
}
}
}
2025-01-17 21:49:23 +00:00
err = cmd.Wait()
2025-02-21 05:03:39 +00:00
if duplicateItem != "" {
err = fmt.Errorf("returned item %s twice: %v", duplicateItem, err)
errItem = makeErrorItem(err, logs)
log.Printf("error: %v", err)
return nil, nil, errItem, err
} else if ctx.Err() == context.DeadlineExceeded {
2025-02-10 16:22:19 +00:00
err = fmt.Errorf("timed out after %v", timeout)
errItem = makeErrorItem(err, logs)
log.Printf("error: %v", err)
return nil, nil, errItem, err
2025-01-17 21:49:23 +00:00
} else if exiterr, ok := err.(*exec.ExitError); ok {
2025-02-10 16:22:19 +00:00
err = fmt.Errorf("%s failed with exit code %d: %v", argv[0], exiterr.ExitCode(), exiterr)
errItem = makeErrorItem(err, logs)
log.Printf("error: %v", err)
return nil, nil, errItem, err
2025-01-17 21:49:23 +00:00
} else if err != nil {
2025-02-10 16:22:19 +00:00
err = fmt.Errorf("error: %s failed with error: %s", argv[0], err)
errItem = makeErrorItem(err, logs)
log.Printf("error: %v", err)
return nil, nil, errItem, err
2025-01-17 21:49:23 +00:00
}
if parseError {
2025-02-10 16:22:19 +00:00
err = fmt.Errorf("could not parse an item")
errItem = makeErrorItem(err, logs)
log.Printf("error: %v", err)
return nil, nil, errItem, err
}
2025-02-10 06:59:04 +00:00
newState, err = os.ReadFile(stateFile.Name())
if err != nil {
2025-02-10 16:22:19 +00:00
err = fmt.Errorf("error: failed to read state file: %v", err)
errItem = makeErrorItem(err, logs)
log.Printf("error: %v", err)
return nil, nil, errItem, err
2025-01-17 21:49:23 +00:00
}
2025-02-10 06:59:04 +00:00
return
2025-01-17 21:49:23 +00:00
}
2025-01-30 07:14:00 +00:00
// Execute an action that takes an item as input and returns the item modified.
// This is basically just a wrapper over [Execute] that handles the input and backfilling.
2025-01-30 07:14:00 +00:00
func ExecuteItemAction(
item Item,
argv []string,
2025-01-30 07:51:06 +00:00
env []string,
state []byte,
2025-01-30 07:14:00 +00:00
timeout time.Duration,
2025-02-05 21:21:31 +00:00
postProcess func(item Item) Item,
2025-02-10 16:22:19 +00:00
) (
newItem Item,
newState []byte,
errItem Item,
err error,
) {
makeErrorItem := makeMakeErrItem(item.Source, argv)
2025-01-30 07:14:00 +00:00
itemJson, err := json.Marshal(item)
if err != nil {
2025-02-10 16:22:19 +00:00
err = fmt.Errorf("failed to serialize item: %v", err)
errItem = makeErrorItem(err, nil)
return
2025-01-30 07:14:00 +00:00
}
2025-02-10 16:22:19 +00:00
res, newState, errItem, err := Execute(item.Source, argv, env, state, string(itemJson), timeout, postProcess)
2025-01-30 07:14:00 +00:00
if err != nil {
2025-02-10 16:22:19 +00:00
err = fmt.Errorf("failed to execute action for %s/%s: %v", item.Source, item.Id, err)
errItem = makeErrorItem(err, nil)
return
2025-01-30 07:14:00 +00:00
}
if len(res) != 1 {
2025-02-10 16:22:19 +00:00
err = fmt.Errorf("expected action to produce exactly one item, got %d", len(res))
errItem = makeErrorItem(err, nil)
return
2025-01-30 07:14:00 +00:00
}
2025-02-10 16:22:19 +00:00
newItem = res[0]
2025-01-30 07:14:00 +00:00
BackfillItem(&newItem, &item)
2025-02-10 16:22:19 +00:00
return
2025-01-30 07:14:00 +00:00
}