-
Notifications
You must be signed in to change notification settings - Fork 6
/
run.go
293 lines (232 loc) · 7.21 KB
/
run.go
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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
package console
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"syscall"
"github.com/kballard/go-shellquote"
"github.com/spf13/cobra"
)
// Start - Start the console application (readline loop). Blocking.
// The error returned will always be an error that the console
// application does not understand or cannot handle.
func (c *Console) Start() error {
return c.StartContext(context.Background())
}
// StartContext is like console.Start(). with a user-provided context.
func (c *Console) StartContext(ctx context.Context) error {
c.loadActiveHistories()
// Print the console logo
if c.printLogo != nil {
c.printLogo(c)
}
lastLine := "" // used to check if last read line is empty.
for {
c.displayPostRun(lastLine)
// Always ensure we work with the active menu, with freshly
// generated commands, bound prompts and some other things.
menu := c.activeMenu()
menu.resetPreRun()
if err := c.runAllE(c.PreReadlineHooks); err != nil {
menu.ErrorHandler(PreReadError{newError(err, "Pre-read error")})
continue
}
// Block and read user input.
line, err := c.shell.Readline()
c.displayPostRun(line)
if err != nil {
menu.handleInterrupt(err)
lastLine = line
continue
}
// Any call to the SwitchMenu() while we were reading user
// input (through an interrupt handler) might have changed it,
// so we must be sure we use the good one.
menu = c.activeMenu()
// Parse the line with bash-syntax, removing comments.
args, err := c.parse(line)
if err != nil {
menu.ErrorHandler(ParseError{newError(err, "Parsing error")})
continue
}
if len(args) == 0 {
lastLine = line
continue
}
// Run user-provided pre-run line hooks,
// which may modify the input line args.
args, err = c.runLineHooks(args)
if err != nil {
menu.ErrorHandler(LineHookError{newError(err, "Line error")})
continue
}
// Run all pre-run hooks and the command itself
// Don't check the error: if its a cobra error,
// the library user is responsible for setting
// the cobra behavior.
// If it's an interrupt, we take care of it.
if err := c.execute(ctx, menu, args, false); err != nil {
menu.ErrorHandler(ExecutionError{newError(err, "")})
}
lastLine = line
}
}
// RunCommandArgs is a convenience function to run a command line in a given menu.
// After running, the menu's commands are reset, and the prompts reloaded, therefore
// mostly mimicking the behavior that is the one of the normal readline/run/readline
// workflow.
// Although state segregation is a priority for this library to be ensured as much
// as possible, you should be cautious when using this function to run commands.
func (m *Menu) RunCommandArgs(ctx context.Context, args []string) (err error) {
// The menu used and reset is the active menu.
// Prepare its output buffer for the command.
m.resetPreRun()
// Run the command and associated helpers.
return m.console.execute(ctx, m, args, !m.console.isExecuting)
}
// RunCommandLine is the equivalent of menu.RunCommandArgs(), but accepts
// an unsplit command line to execute. This line is split and processed in
// *sh-compliant form, identically to how lines are in normal console usage.
func (m *Menu) RunCommandLine(ctx context.Context, line string) (err error) {
if len(line) == 0 {
return
}
// Split the line into shell words.
args, err := shellquote.Split(line)
if err != nil {
return fmt.Errorf("line error: %w", err)
}
return m.RunCommandArgs(ctx, args)
}
// execute - The user has entered a command input line, the arguments have been processed:
// we synchronize a few elements of the console, then pass these arguments to the command
// parser for execution and error handling.
// Our main object of interest is the menu's root command, and we explicitly use this reference
// instead of the menu itself, because if RunCommand() is asynchronously triggered while another
// command is running, the menu's root command will be overwritten.
func (c *Console) execute(ctx context.Context, menu *Menu, args []string, async bool) error {
if !async {
c.mutex.RLock()
c.isExecuting = true
c.mutex.RUnlock()
}
defer func() {
c.mutex.RLock()
c.isExecuting = false
c.mutex.RUnlock()
}()
// Our root command of interest, used throughout this function.
cmd := menu.Command
// Find the target command: if this command is filtered, don't run it.
target, _, _ := cmd.Find(args)
if err := menu.CheckIsAvailable(target); err != nil {
return err
}
// Reset all flags to their default values.
resetFlagsDefaults(target)
// Console-wide pre-run hooks, cannot.
if err := c.runAllE(c.PreCmdRunHooks); err != nil {
return fmt.Errorf("pre-run error: %s", err.Error())
}
// Assign those arguments to our parser.
cmd.SetArgs(args)
// The command execution should happen in a separate goroutine,
// and should notify the main goroutine when it is done.
ctx, cancel := context.WithCancelCause(ctx)
cmd.SetContext(ctx)
// Start monitoring keyboard and OS signals.
sigchan := c.monitorSignals()
// And start the command execution.
go c.executeCommand(cmd, cancel)
// Wait for the command to finish, or for an OS signal to be caught.
select {
case <-ctx.Done():
cause := context.Cause(ctx)
if !errors.Is(cause, context.Canceled) {
return cause
}
case signal := <-sigchan:
cancel(errors.New(signal.String()))
menu.handleInterrupt(errors.New(signal.String()))
}
return nil
}
// Run the command in a separate goroutine, and cancel the context when done.
func (c *Console) executeCommand(cmd *cobra.Command, cancel context.CancelCauseFunc) {
if err := cmd.Execute(); err != nil {
cancel(err)
return
}
// And the post-run hooks in the same goroutine,
// because they should not be skipped even if
// the command is backgrounded by the user.
if err := c.runAllE(c.PostCmdRunHooks); err != nil {
cancel(err)
return
}
// Command successfully executed, cancel the context.
cancel(nil)
}
func (c *Console) loadActiveHistories() {
c.shell.History.Delete()
for _, name := range c.activeMenu().historyNames {
c.shell.History.Add(name, c.activeMenu().histories[name])
}
}
func (c *Console) runAllE(hooks []func() error) error {
for _, hook := range hooks {
if err := hook(); err != nil {
return err
}
}
return nil
}
func (c *Console) runLineHooks(args []string) ([]string, error) {
processed := args
// Or modify them again
for _, hook := range c.PreCmdRunLineHooks {
var err error
if processed, err = hook(processed); err != nil {
return nil, err
}
}
return processed, nil
}
func (c *Console) displayPreRun(line string) {
if c.NewlineBefore {
if !c.NewlineWhenEmpty {
if !c.lineEmpty(line) {
fmt.Println()
}
} else {
fmt.Println()
}
}
}
func (c *Console) displayPostRun(lastLine string) {
if c.NewlineAfter {
if !c.NewlineWhenEmpty {
if !c.lineEmpty(lastLine) {
fmt.Println()
}
} else {
fmt.Println()
}
}
c.printed = false
}
// monitorSignals - Monitor the signals that can be sent to the process
// while a command is running. We want to be able to cancel the command.
func (c *Console) monitorSignals() <-chan os.Signal {
sigchan := make(chan os.Signal, 1)
signal.Notify(
sigchan,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT,
// syscall.SIGKILL,
)
return sigchan
}