Skip to content

Commit b532382

Browse files
committed
persist env vars between serial tasks
1 parent c90ff8f commit b532382

File tree

3 files changed

+56
-17
lines changed

3 files changed

+56
-17
lines changed

example/scripts/setvar.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#!/bin/bash
22

3-
export VAR2=isnowalsoset
3+
export VAR2=isnowalsoset

main.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ func bundle(userYamlPath, outputPath string) {
142142

143143
bashfulPath, err := os.Executable()
144144
CheckError(err, "Could not find path to bashful")
145-
err = archiver.TarGz.Make(archivePath, []string{userYamlPath, bashfulPath, config.cachePath})
145+
err = archiver.TarGz.Make(archivePath, []string{userYamlPath, bashfulPath, config.CachePath})
146146
CheckError(err, "Unable to create bundle")
147147

148148
execute := `#!/bin/bash
@@ -227,8 +227,12 @@ func run(userYamlPath string) {
227227
fmt.Println(bold("Running " + userYamlPath + tagInfo))
228228
logToMain("Running "+userYamlPath+tagInfo, MAJOR_FORMAT)
229229

230+
// Since this is an empty map, no env vars will be loaded explicitly into the first exec.Command
231+
// which will cause the current processes env vars to be loaded instead
232+
environment := map[string]string{}
233+
230234
for _, task := range AllTasks {
231-
task.Run()
235+
task.Run(environment)
232236
failedTasks = append(failedTasks, task.failedTasks...)
233237

234238
if exitSignaled {
@@ -260,10 +264,10 @@ func run(userYamlPath string) {
260264
for _, task := range failedTasks {
261265

262266
buffer.WriteString("\n")
263-
buffer.WriteString(bold(red(" Failed task: ")) + bold(task.Config.Name) + "\n")
267+
buffer.WriteString(bold(red(" Failed task: ")) + bold(task.Config.Name) + "\n")
264268
buffer.WriteString(red(" ├─ command: ") + task.Config.CmdString + "\n")
265269
buffer.WriteString(red(" ├─ return code: ") + strconv.Itoa(task.Command.ReturnCode) + "\n")
266-
buffer.WriteString(red(" ─ stderr: ") + task.ErrorBuffer.String() + "\n")
270+
buffer.WriteString(red(" ─ stderr: ") + task.ErrorBuffer.String() + "\n")
267271

268272
}
269273
logToMain(buffer.String(), "")

task.go

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ var (
3636
lineParallelTemplate, _ = template.New("parallel line").Parse(` {{.Status}} ` + color.Reset + ` {{printf "%1s" .Prefix}} ├─ {{printf "%-25s" .Title}} {{.Msg}}{{.Split}}{{.Eta}}`)
3737

3838
// lineLastParallelTemplate is the string template used to display the status values of a task that is the LAST child of another task
39-
lineLastParallelTemplate, _ = template.New("last parallel line").Parse(` {{.Status}} ` + color.Reset + ` {{printf "%1s" .Prefix}} ─ {{printf "%-25s" .Title}} {{.Msg}}{{.Split}}{{.Eta}}`)
39+
lineLastParallelTemplate, _ = template.New("last parallel line").Parse(` {{.Status}} ` + color.Reset + ` {{printf "%1s" .Prefix}} ─ {{printf "%-25s" .Title}} {{.Msg}}{{.Split}}{{.Eta}}`)
4040
)
4141

4242
// TaskStats is a global struct keeping track of the number of running tasks, failed tasks, completed tasks, and total tasks
@@ -130,6 +130,12 @@ type TaskCommand struct {
130130

131131
// ReturnCode is simply the value returned from the child process after Cmd execution
132132
ReturnCode int
133+
134+
// EnvReadFile is an extra pipe given to the child shell process for exfiltrating env vars back up to bashful (to provide as input for future tasks)
135+
EnvReadFile *os.File
136+
137+
// Environment is a list of env vars from the exited child process
138+
Environment map[string]string
133139
}
134140

135141
// CommandStatus represents whether a task command is about to run, already running, or has completed (in which case, was it successful or not)
@@ -252,12 +258,20 @@ func (task *Task) inflateCmd() {
252258
shell = "sh"
253259
}
254260

255-
task.Command.Cmd = exec.Command(shell, "-c", fmt.Sprintf("\"%q\"", task.Config.CmdString))
261+
readFd, writeFd, err := os.Pipe()
262+
CheckError(err, "Could not open env pipe for child shell")
263+
264+
task.Command.Cmd = exec.Command(shell, "-c", task.Config.CmdString+"; env >&3")
265+
266+
// allow the child process to provide env vars via a pipe (FD3)
267+
task.Command.Cmd.ExtraFiles = []*os.File{writeFd}
268+
task.Command.EnvReadFile = readFd
256269

257270
// set this command as a process group
258271
task.Command.Cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
259272

260273
task.Command.ReturnCode = -1
274+
task.Command.Environment = map[string]string{}
261275
}
262276

263277
func (task *Task) UpdateExec(execpath string) {
@@ -440,7 +454,7 @@ func variableSplitFunc(data []byte, atEOF bool) (advance int, token []byte, err
440454
}
441455

442456
// runSingleCmd executes a tasks primary command (not child task commands) and monitors command events
443-
func (task *Task) runSingleCmd(resultChan chan CmdEvent, waiter *sync.WaitGroup) {
457+
func (task *Task) runSingleCmd(resultChan chan CmdEvent, waiter *sync.WaitGroup, environment map[string]string) {
444458
logToMain("Started Task: "+task.Config.Name, INFO_FORMAT)
445459

446460
task.Command.StartTime = time.Now()
@@ -457,6 +471,11 @@ func (task *Task) runSingleCmd(resultChan chan CmdEvent, waiter *sync.WaitGroup)
457471
stdoutPipe, _ := task.Command.Cmd.StdoutPipe()
458472
stderrPipe, _ := task.Command.Cmd.StderrPipe()
459473

474+
// copy env vars into proc
475+
for k, v := range environment {
476+
task.Command.Cmd.Env = append(task.Command.Cmd.Env, fmt.Sprintf("%s=%s", k, v))
477+
}
478+
460479
task.Command.Cmd.Start()
461480

462481
var readPipe func(chan string, io.ReadCloser)
@@ -539,6 +558,23 @@ func (task *Task) runSingleCmd(resultChan chan CmdEvent, waiter *sync.WaitGroup)
539558

540559
logToMain("Completed Task: "+task.Config.Name+" (rc: "+returnCodeMsg+")", INFO_FORMAT)
541560

561+
// close the write end of the pipe since the child shell is positively no longer writting to it
562+
task.Command.Cmd.ExtraFiles[0].Close()
563+
data, err := ioutil.ReadAll(task.Command.EnvReadFile)
564+
CheckError(err, "Could not read env vars from child shell")
565+
566+
if environment != nil {
567+
lines := strings.Split(string(data[:]), "\n")
568+
for _, line := range lines {
569+
fields := strings.SplitN(strings.TrimSpace(line), "=", 2)
570+
if len(fields) == 2 {
571+
environment[fields[0]] = fields[1]
572+
} else if len(fields) == 1 {
573+
environment[fields[0]] = ""
574+
}
575+
}
576+
}
577+
542578
if returnCode == 0 || task.Config.IgnoreFailure {
543579
resultChan <- CmdEvent{Task: task, Status: StatusSuccess, Complete: true, ReturnCode: returnCode}
544580
} else {
@@ -551,7 +587,6 @@ func (task *Task) runSingleCmd(resultChan chan CmdEvent, waiter *sync.WaitGroup)
551587

552588
// Pave prints the initial task (and child task) formatted status to the screen using newline characters to advance rows (not ansi control codes)
553589
func (task *Task) Pave() {
554-
logToMain(" Pave Task: "+task.Config.Name, MAJOR_FORMAT)
555590
var message bytes.Buffer
556591
hasParentCmd := task.Config.CmdString != ""
557592
hasHeader := len(task.Children) > 0
@@ -582,14 +617,14 @@ func (task *Task) Pave() {
582617
}
583618

584619
// StartAvailableTasks will kick start the maximum allowed number of commands (both primary and child task commands). Repeated invocation will iterate to new commands (and not repeat already completed commands)
585-
func (task *Task) StartAvailableTasks() {
620+
func (task *Task) StartAvailableTasks(environment map[string]string) {
586621
if task.Config.CmdString != "" && !task.Command.Started && TaskStats.runningCmds < config.Options.MaxParallelCmds {
587-
go task.runSingleCmd(task.resultChan, &task.waiter)
622+
go task.runSingleCmd(task.resultChan, &task.waiter, environment)
588623
task.Command.Started = true
589624
TaskStats.runningCmds++
590625
}
591626
for ; TaskStats.runningCmds < config.Options.MaxParallelCmds && task.lastStartedTask < len(task.Children); task.lastStartedTask++ {
592-
go task.Children[task.lastStartedTask].runSingleCmd(task.resultChan, &task.waiter)
627+
go task.Children[task.lastStartedTask].runSingleCmd(task.resultChan, &task.waiter, nil)
593628
task.Children[task.lastStartedTask].Command.Started = true
594629
TaskStats.runningCmds++
595630
}
@@ -607,7 +642,7 @@ func (task *Task) Completed(rc int) {
607642
}
608643

609644
// listenAndDisplay updates the screen frame with the latest task and child task updates as they occur (either in realtime or in a polling loop). Returns when all child processes have been completed.
610-
func (task *Task) listenAndDisplay() {
645+
func (task *Task) listenAndDisplay(environment map[string]string) {
611646
scr := Screen()
612647
// just wait for stuff to come back
613648
for TaskStats.runningCmds > 0 {
@@ -642,7 +677,7 @@ func (task *Task) listenAndDisplay() {
642677
// update the state before displaying...
643678
if msgObj.Complete {
644679
eventTask.Completed(msgObj.ReturnCode)
645-
task.StartAvailableTasks()
680+
task.StartAvailableTasks(environment)
646681
task.status = msgObj.Status
647682
if msgObj.Status == StatusError {
648683
// update the group status to indicate a failed subtask
@@ -688,13 +723,13 @@ func (task *Task) listenAndDisplay() {
688723
}
689724

690725
// Run will run the current tasks primary command and/or all child commands. When execution has completed, the screen frame will advance.
691-
func (task *Task) Run() {
726+
func (task *Task) Run(environment map[string]string) {
692727

693728
var message bytes.Buffer
694729

695730
task.Pave()
696-
task.StartAvailableTasks()
697-
task.listenAndDisplay()
731+
task.StartAvailableTasks(environment)
732+
task.listenAndDisplay(environment)
698733

699734
scr := Screen()
700735
hasHeader := len(task.Children) > 0

0 commit comments

Comments
 (0)