1
2
3 package cmd
4
5 import (
6 "bytes"
7 "context"
8 "fmt"
9 "io"
10 "strings"
11 "sync/atomic"
12 "time"
13
14 "github.com/Microsoft/hcsshim/internal/cow"
15 hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
16 "github.com/Microsoft/hcsshim/internal/log"
17 specs "github.com/opencontainers/runtime-spec/specs-go"
18 "github.com/sirupsen/logrus"
19 "golang.org/x/sync/errgroup"
20 "golang.org/x/sys/windows"
21 )
22
23
24 type CmdProcessRequest struct {
25 Args []string
26 Workdir string
27 Terminal bool
28 Stdin string
29 Stdout string
30 Stderr string
31 }
32
33
34 type Cmd struct {
35
36 Host cow.ProcessHost
37
38
39 Spec *specs.Process
40
41
42 Stdin io.Reader
43 Stdout io.Writer
44 Stderr io.Writer
45
46
47 Log *logrus.Entry
48
49
50 Context context.Context
51
52
53
54
55
56
57 CopyAfterExitTimeout time.Duration
58
59
60 Process cow.Process
61
62
63 ExitState *ExitState
64
65 iogrp errgroup.Group
66 stdinErr atomic.Value
67 allDoneCh chan struct{}
68 }
69
70
71 type ExitState struct {
72 exited bool
73 code int
74 }
75
76
77 func (s *ExitState) ExitCode() int {
78 if !s.exited {
79 return -1
80 }
81 return s.code
82 }
83
84
85 type ExitError struct {
86 *ExitState
87 }
88
89 func (err *ExitError) Error() string {
90 return fmt.Sprintf("process exited with exit code %d", err.ExitCode())
91 }
92
93
94 type lcowProcessParameters struct {
95 hcsschema.ProcessParameters
96 OCIProcess *specs.Process `json:"OciProcess,omitempty"`
97 }
98
99
100 func escapeArgs(args []string) string {
101 escapedArgs := make([]string, len(args))
102 for i, a := range args {
103 escapedArgs[i] = windows.EscapeArg(a)
104 }
105 return strings.Join(escapedArgs, " ")
106 }
107
108
109 func Command(host cow.ProcessHost, name string, arg ...string) *Cmd {
110 cmd := &Cmd{
111 Host: host,
112 Spec: &specs.Process{
113 Args: append([]string{name}, arg...),
114 },
115 Log: log.L.Dup(),
116 ExitState: &ExitState{},
117 }
118 if host.OS() == "windows" {
119 cmd.Spec.Cwd = `C:\`
120 } else {
121 cmd.Spec.Cwd = "/"
122 cmd.Spec.Env = []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}
123 }
124 return cmd
125 }
126
127
128
129 func CommandContext(ctx context.Context, host cow.ProcessHost, name string, arg ...string) *Cmd {
130 cmd := Command(host, name, arg...)
131 cmd.Context = ctx
132 cmd.Log = log.G(ctx)
133 return cmd
134 }
135
136
137
138 func (c *Cmd) Start() error {
139 c.allDoneCh = make(chan struct{})
140 var x interface{}
141 if !c.Host.IsOCI() {
142 wpp := &hcsschema.ProcessParameters{
143 CommandLine: c.Spec.CommandLine,
144 User: c.Spec.User.Username,
145 WorkingDirectory: c.Spec.Cwd,
146 EmulateConsole: c.Spec.Terminal,
147 CreateStdInPipe: c.Stdin != nil,
148 CreateStdOutPipe: c.Stdout != nil,
149 CreateStdErrPipe: c.Stderr != nil,
150 }
151
152 if c.Spec.CommandLine == "" {
153 if c.Host.OS() == "windows" {
154 wpp.CommandLine = escapeArgs(c.Spec.Args)
155 } else {
156 wpp.CommandArgs = c.Spec.Args
157 }
158 }
159
160 environment := make(map[string]string)
161 for _, v := range c.Spec.Env {
162 s := strings.SplitN(v, "=", 2)
163 if len(s) == 2 && len(s[1]) > 0 {
164 environment[s[0]] = s[1]
165 }
166 }
167 wpp.Environment = environment
168
169 if c.Spec.ConsoleSize != nil {
170 wpp.ConsoleSize = []int32{
171 int32(c.Spec.ConsoleSize.Height),
172 int32(c.Spec.ConsoleSize.Width),
173 }
174 }
175 x = wpp
176 } else {
177 lpp := &lcowProcessParameters{
178 ProcessParameters: hcsschema.ProcessParameters{
179 CreateStdInPipe: c.Stdin != nil,
180 CreateStdOutPipe: c.Stdout != nil,
181 CreateStdErrPipe: c.Stderr != nil,
182 },
183 OCIProcess: c.Spec,
184 }
185 x = lpp
186 }
187 if c.Context != nil && c.Context.Err() != nil {
188 return c.Context.Err()
189 }
190 p, err := c.Host.CreateProcess(context.TODO(), x)
191 if err != nil {
192 return err
193 }
194 c.Process = p
195 if c.Log != nil {
196 c.Log = c.Log.WithField("pid", p.Pid())
197 }
198
199
200 stdin, stdout, stderr := p.Stdio()
201 if c.Stdin != nil {
202
203
204
205 go func() {
206 _, err := relayIO(stdin, c.Stdin, c.Log, "stdin")
207
208
209
210 if err != nil {
211 c.stdinErr.Store(err)
212 }
213
214 if err := p.CloseStdin(context.TODO()); err != nil && c.Log != nil {
215 c.Log.WithError(err).Warn("failed to close Cmd stdin")
216 }
217 }()
218 }
219
220 if c.Stdout != nil {
221 c.iogrp.Go(func() error {
222 _, err := relayIO(c.Stdout, stdout, c.Log, "stdout")
223 if err := p.CloseStdout(context.TODO()); err != nil {
224 c.Log.WithError(err).Warn("failed to close Cmd stdout")
225 }
226 return err
227 })
228 }
229
230 if c.Stderr != nil {
231 c.iogrp.Go(func() error {
232 _, err := relayIO(c.Stderr, stderr, c.Log, "stderr")
233 if err := p.CloseStderr(context.TODO()); err != nil {
234 c.Log.WithError(err).Warn("failed to close Cmd stderr")
235 }
236 return err
237 })
238 }
239
240 if c.Context != nil {
241 go func() {
242 select {
243 case <-c.Context.Done():
244
245
246 ctx := c.Context
247 if ctx == nil {
248 ctx = context.Background()
249 }
250 kctx := log.Copy(context.Background(), ctx)
251 _, _ = c.Process.Kill(kctx)
252 case <-c.allDoneCh:
253 }
254 }()
255 }
256 return nil
257 }
258
259
260
261
262 func (c *Cmd) Wait() error {
263 waitErr := c.Process.Wait()
264 if waitErr != nil && c.Log != nil {
265 c.Log.WithError(waitErr).Warn("process wait failed")
266 }
267 state := &ExitState{}
268 code, exitErr := c.Process.ExitCode()
269 if exitErr == nil {
270 state.exited = true
271 state.code = code
272 }
273
274 if c.CopyAfterExitTimeout != 0 {
275 go func() {
276 t := time.NewTimer(c.CopyAfterExitTimeout)
277 defer t.Stop()
278 select {
279 case <-c.allDoneCh:
280 case <-t.C:
281
282 c.Process.Close()
283 if c.Log != nil {
284 c.Log.Warn("timed out waiting for stdio relay")
285 }
286 }
287 }()
288 }
289 ioErr := c.iogrp.Wait()
290 if ioErr == nil {
291 ioErr, _ = c.stdinErr.Load().(error)
292 }
293 close(c.allDoneCh)
294 c.Process.Close()
295 c.ExitState = state
296 if exitErr != nil {
297 return exitErr
298 }
299 if state.exited && state.code != 0 {
300 return &ExitError{state}
301 }
302 return ioErr
303 }
304
305
306 func (c *Cmd) Run() error {
307 err := c.Start()
308 if err != nil {
309 return err
310 }
311 return c.Wait()
312 }
313
314
315
316 func (c *Cmd) Output() ([]byte, error) {
317 var b bytes.Buffer
318 c.Stdout = &b
319 err := c.Run()
320 return b.Bytes(), err
321 }
322
View as plain text