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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
|
package shell
import (
"errors"
"fmt"
"time"
"github.com/go-cmd/cmd"
"go.wit.com/log"
)
// 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
}
*/
// shortcut, sends a blank value for pwd
// which means the exec Dir is not set
// echos output (otherwise use RunQuiet)
func Run(argv []string) cmd.Status {
return PathRun("", argv)
}
// exec the cmd at a filepath. this does not change the working directory
// sets the exec dir if it's not an empty string
// combines stdout and stderr
// echo's output (otherwise use PathRunQuiet()
// this is basically the exact example from the go-cmd/cmd devs
// where the have rocked out a proper smart read on both filehandles
// https://dave.cheney.net/2013/04/30/curious-channels
func PathRun(path string, argv []string) cmd.Status {
return PathRunLog(path, argv, NOW)
}
// the actual wrapper around go-cmd/cmd
// adds a log Flag so that echo to stdout can be enabled/disabled
func PathRunLog(path string, argv []string, logf *log.LogFlag) cmd.Status {
var save []string // combined stdout & stderr
var arg0 string
var args []string
if logf == nil {
logf = NOW
}
log.Log(logf, "shell.PathRunLog() Path =", path, "cmd =", argv)
// Check if the slice has at least one element (the command name)
if len(argv) == 0 {
var s cmd.Status
s.Error = errors.New("Error: Command slice is empty.")
return s
}
if len(argv) == 1 {
// Pass the first element as the command, and the rest as variadic arguments
arg0 = argv[0]
} else {
arg0 = argv[0]
args = argv[1:]
}
// Disable output buffering, enable streaming
cmdOptions := cmd.Options{
Buffered: false,
Streaming: true,
}
// Create Cmd with options
envCmd := cmd.NewCmdOptions(cmdOptions, arg0, args...)
if path != "" {
// set the path for exec
envCmd.Dir = path
}
// 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)
log.Log(logf, line)
// fmt.Println(line)
case line, open := <-envCmd.Stderr:
if !open {
envCmd.Stderr = nil
continue
}
save = append(save, line)
log.Log(logf, 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
}
// uses the 'log' package to disable echo to STDOUT
// only echos if you enable the shell.INFO log flag
func RunQuiet(args []string) cmd.Status {
return PathRunLog("", args, INFO)
}
// uses the 'log' package to disable echo to STDOUT
// only echos if you enable the shell.INFO log flag
func PathRunQuiet(pwd string, args []string) cmd.Status {
return PathRunLog(pwd, args, INFO)
}
// send blank path == use current golang working directory
func RunRealtime(args []string) cmd.Status {
return PathRunRealtime("", args)
}
// echos twice a second if anything sends to STDOUT or STDERR
// not great, but it's really just for watching things run in real time anyway
// TODO: fix \r handling for things like git-clone so the terminal doesn't
// have to do a \n newline each time.
// TODO: add timeouts and status of things hanging around forever
func PathRunRealtime(pwd string, args []string) cmd.Status {
// 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
}
// Start a long-running process, capture stdout and stderr
a, b := RemoveFirstElement(args)
findCmd := cmd.NewCmd(a, b...)
if pwd != "" {
findCmd.Dir = pwd
}
statusChan := findCmd.Start() // non-blocking
ticker := time.NewTicker(5 * time.Millisecond)
// this is interesting, maybe useful, but wierd, but neat. interesting even
// Print last line of stdout every 2s
go func() {
// loop very quickly, but only print the line if it changes
var lastout string
var lasterr string
for range ticker.C {
status := findCmd.Status()
n := len(status.Stdout)
if n != 0 {
newline := status.Stdout[n-1]
if lastout != newline {
lastout = newline
log.Info(lastout)
}
}
n = len(status.Stderr)
if n != 0 {
newline := status.Stderr[n-1]
if lasterr != newline {
lasterr = newline
log.Info(lasterr)
}
}
if status.Complete {
return
}
}
}()
// 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")
}
// this is stuff from a long time ago that there must be a replacement for
func RemoveFirstElement(slice []string) (string, []string) {
if len(slice) == 0 {
return "", slice // Return the original slice if it's empty
}
return slice[0], slice[1:] // Return the slice without the first element
}
|