summaryrefslogtreecommitdiff
path: root/cmd.go
blob: faaa3c6b0c85936b7466b708daeb95cb394c01ed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package main

// this is a simplified interaction with the excellent
// go-cmd/cmd package to work 'shell' like.

// in all cases here, STDERR -> STDOUT
// If you want the output from whatever you run
// to be captured like it appears when you see it
// on the command line, this is what this tries to do

/*
	if r := shell.Run([]{"ping", "-c", "3", "localhost"}); r.Error == nil {
		if r.Exit == 0 {
			log.Println("ran ok")
		} else {
			log.Println("ran")
		}
		// all stdout/stderr captured in r.Stdout
	}
*/

import (
	"errors"
	"fmt"
	"time"

	"github.com/go-cmd/cmd"
	"go.wit.com/log"
)

func Run(args []string) cmd.Status {
	return PwdRun("", args)
}

// absolutely doesn't echo anything
func PwdRunQuiet(pwd string, args []string) cmd.Status {
	var arg0 string
	var argx []string
	// Check if the slice has at least one element (the command name)
	if len(args) == 0 {
		var s cmd.Status
		s.Error = errors.New("Error: Command slice is empty.")
		return s
	}
	if len(args) == 1 {
		// Pass the first element as the command, and the rest as variadic arguments
		arg0 = args[0]
	} else {
		arg0 = args[0]
		argx = args[1:]
	}

	// Start a long-running process, capture stdout and stderr
	findCmd := cmd.NewCmd(arg0, argx...)
	if pwd != "" {
		findCmd.Dir = pwd
	}
	statusChan := findCmd.Start() // non-blocking

	ticker := time.NewTicker(2 * time.Second)

	// this is interesting, maybe useful, but wierd, but neat. interesting even
	// Print last line of stdout every 2s
	go func() {
		for range ticker.C {
			status := findCmd.Status()
			n := len(status.Stdout)
			if n != 0 {
				fmt.Println(status.Stdout[n-1])
			}
		}
	}()

	// Stop command after 1 hour
	go func() {
		<-time.After(1 * time.Hour)
		findCmd.Stop()
	}()

	// Check if command is done
	select {
	case finalStatus := <-statusChan:
		log.Info("finalStatus =", finalStatus.Exit, finalStatus.Error)
		return finalStatus
		// done
	default:
		// no, still running
	}

	// Block waiting for command to exit, be stopped, or be killed
	finalStatus := <-statusChan
	return finalStatus
}

func blah(cmd []string) {
	r := Run(cmd)
	log.Info("cmd =", r.Cmd)
	log.Info("complete =", r.Complete)
	log.Info("exit =", r.Exit)
	log.Info("err =", r.Error)
	log.Info("len(stdout+stderr) =", len(r.Stdout))
}

// run these to see confirm the sytem behaves as expected
func RunTest() {
	blah([]string{"ping", "-c", "3", "localhost"})
	blah([]string{"exit", "0"})
	blah([]string{"exit", "-1"})
	blah([]string{"true"})
	blah([]string{"false"})
	blah([]string{"grep", "root", "/etc/", "/proc/cmdline", "/usr/bin/chmod"})
	blah([]string{"grep", "root", "/proc/cmdline"})
	fmt.Sprint("blahdone")
}

// sets the exec dir if it's sent
// combines stdout and stderr
// echo's output
func PwdRun(pwd string, args []string) cmd.Status {
	var save []string // combined stdout & stderr
	var arg0 string
	var argx []string
	// Check if the slice has at least one element (the command name)
	if len(args) == 0 {
		var s cmd.Status
		s.Error = errors.New("Error: Command slice is empty.")
		return s
	}
	if len(args) == 1 {
		// Pass the first element as the command, and the rest as variadic arguments
		arg0 = args[0]
	} else {
		arg0 = args[0]
		argx = args[1:]
	}

	// Disable output buffering, enable streaming
	cmdOptions := cmd.Options{
		Buffered:  false,
		Streaming: true,
	}

	// Create Cmd with options
	envCmd := cmd.NewCmdOptions(cmdOptions, arg0, argx...)
	if pwd != "" {
		envCmd.Dir = pwd
	}

	// Print STDOUT and STDERR lines streaming from Cmd
	doneChan := make(chan struct{})
	go func() {
		defer close(doneChan)
		// Done when both channels have been closed
		// https://dave.cheney.net/2013/04/30/curious-channels
		for envCmd.Stdout != nil || envCmd.Stderr != nil {
			select {
			case line, open := <-envCmd.Stdout:
				if !open {
					envCmd.Stdout = nil
					continue
				}
				save = append(save, line)
				fmt.Println(line)
			case line, open := <-envCmd.Stderr:
				if !open {
					envCmd.Stderr = nil
					continue
				}
				save = append(save, line)
				fmt.Println(line)
			}
		}
	}()

	// Run and wait for Cmd to return, discard Status
	<-envCmd.Start()

	// Wait for goroutine to print everything
	<-doneChan

	s := envCmd.Status()
	s.Stdout = save
	return s
}