1package hcs
2
3import (
4	"context"
5	"encoding/json"
6	"io"
7	"sync"
8	"syscall"
9	"time"
10
11	"github.com/Microsoft/hcsshim/internal/log"
12	"github.com/Microsoft/hcsshim/internal/oc"
13	"github.com/Microsoft/hcsshim/internal/vmcompute"
14	"go.opencensus.io/trace"
15)
16
17// ContainerError is an error encountered in HCS
18type Process struct {
19	handleLock     sync.RWMutex
20	handle         vmcompute.HcsProcess
21	processID      int
22	system         *System
23	hasCachedStdio bool
24	stdioLock      sync.Mutex
25	stdin          io.WriteCloser
26	stdout         io.ReadCloser
27	stderr         io.ReadCloser
28	callbackNumber uintptr
29
30	closedWaitOnce sync.Once
31	waitBlock      chan struct{}
32	exitCode       int
33	waitError      error
34}
35
36func newProcess(process vmcompute.HcsProcess, processID int, computeSystem *System) *Process {
37	return &Process{
38		handle:    process,
39		processID: processID,
40		system:    computeSystem,
41		waitBlock: make(chan struct{}),
42	}
43}
44
45type processModifyRequest struct {
46	Operation   string
47	ConsoleSize *consoleSize `json:",omitempty"`
48	CloseHandle *closeHandle `json:",omitempty"`
49}
50
51type consoleSize struct {
52	Height uint16
53	Width  uint16
54}
55
56type closeHandle struct {
57	Handle string
58}
59
60type processStatus struct {
61	ProcessID      uint32
62	Exited         bool
63	ExitCode       uint32
64	LastWaitResult int32
65}
66
67const (
68	stdIn  string = "StdIn"
69	stdOut string = "StdOut"
70	stdErr string = "StdErr"
71)
72
73const (
74	modifyConsoleSize string = "ConsoleSize"
75	modifyCloseHandle string = "CloseHandle"
76)
77
78// Pid returns the process ID of the process within the container.
79func (process *Process) Pid() int {
80	return process.processID
81}
82
83// SystemID returns the ID of the process's compute system.
84func (process *Process) SystemID() string {
85	return process.system.ID()
86}
87
88func (process *Process) processSignalResult(ctx context.Context, err error) (bool, error) {
89	switch err {
90	case nil:
91		return true, nil
92	case ErrVmcomputeOperationInvalidState, ErrComputeSystemDoesNotExist, ErrElementNotFound:
93		select {
94		case <-process.waitBlock:
95			// The process exit notification has already arrived.
96		default:
97			// The process should be gone, but we have not received the notification.
98			// After a second, force unblock the process wait to work around a possible
99			// deadlock in the HCS.
100			go func() {
101				time.Sleep(time.Second)
102				process.closedWaitOnce.Do(func() {
103					log.G(ctx).WithError(err).Warn("force unblocking process waits")
104					process.exitCode = -1
105					process.waitError = err
106					close(process.waitBlock)
107				})
108			}()
109		}
110		return false, nil
111	default:
112		return false, err
113	}
114}
115
116// Signal signals the process with `options`.
117//
118// For LCOW `guestrequest.SignalProcessOptionsLCOW`.
119//
120// For WCOW `guestrequest.SignalProcessOptionsWCOW`.
121func (process *Process) Signal(ctx context.Context, options interface{}) (bool, error) {
122	process.handleLock.RLock()
123	defer process.handleLock.RUnlock()
124
125	operation := "hcsshim::Process::Signal"
126
127	if process.handle == 0 {
128		return false, makeProcessError(process, operation, ErrAlreadyClosed, nil)
129	}
130
131	optionsb, err := json.Marshal(options)
132	if err != nil {
133		return false, err
134	}
135
136	resultJSON, err := vmcompute.HcsSignalProcess(ctx, process.handle, string(optionsb))
137	events := processHcsResult(ctx, resultJSON)
138	delivered, err := process.processSignalResult(ctx, err)
139	if err != nil {
140		err = makeProcessError(process, operation, err, events)
141	}
142	return delivered, err
143}
144
145// Kill signals the process to terminate but does not wait for it to finish terminating.
146func (process *Process) Kill(ctx context.Context) (bool, error) {
147	process.handleLock.RLock()
148	defer process.handleLock.RUnlock()
149
150	operation := "hcsshim::Process::Kill"
151
152	if process.handle == 0 {
153		return false, makeProcessError(process, operation, ErrAlreadyClosed, nil)
154	}
155
156	resultJSON, err := vmcompute.HcsTerminateProcess(ctx, process.handle)
157	events := processHcsResult(ctx, resultJSON)
158	delivered, err := process.processSignalResult(ctx, err)
159	if err != nil {
160		err = makeProcessError(process, operation, err, events)
161	}
162	return delivered, err
163}
164
165// waitBackground waits for the process exit notification. Once received sets
166// `process.waitError` (if any) and unblocks all `Wait` calls.
167//
168// This MUST be called exactly once per `process.handle` but `Wait` is safe to
169// call multiple times.
170func (process *Process) waitBackground() {
171	operation := "hcsshim::Process::waitBackground"
172	ctx, span := trace.StartSpan(context.Background(), operation)
173	defer span.End()
174	span.AddAttributes(
175		trace.StringAttribute("cid", process.SystemID()),
176		trace.Int64Attribute("pid", int64(process.processID)))
177
178	var (
179		err      error
180		exitCode = -1
181	)
182
183	err = waitForNotification(ctx, process.callbackNumber, hcsNotificationProcessExited, nil)
184	if err != nil {
185		err = makeProcessError(process, operation, err, nil)
186		log.G(ctx).WithError(err).Error("failed wait")
187	} else {
188		process.handleLock.RLock()
189		defer process.handleLock.RUnlock()
190
191		// Make sure we didnt race with Close() here
192		if process.handle != 0 {
193			propertiesJSON, resultJSON, err := vmcompute.HcsGetProcessProperties(ctx, process.handle)
194			events := processHcsResult(ctx, resultJSON)
195			if err != nil {
196				err = makeProcessError(process, operation, err, events)
197			} else {
198				properties := &processStatus{}
199				err = json.Unmarshal([]byte(propertiesJSON), properties)
200				if err != nil {
201					err = makeProcessError(process, operation, err, nil)
202				} else {
203					if properties.LastWaitResult != 0 {
204						log.G(ctx).WithField("wait-result", properties.LastWaitResult).Warning("non-zero last wait result")
205					} else {
206						exitCode = int(properties.ExitCode)
207					}
208				}
209			}
210		}
211	}
212	log.G(ctx).WithField("exitCode", exitCode).Debug("process exited")
213
214	process.closedWaitOnce.Do(func() {
215		process.exitCode = exitCode
216		process.waitError = err
217		close(process.waitBlock)
218	})
219	oc.SetSpanStatus(span, err)
220}
221
222// Wait waits for the process to exit. If the process has already exited returns
223// the pervious error (if any).
224func (process *Process) Wait() error {
225	<-process.waitBlock
226	return process.waitError
227}
228
229// ResizeConsole resizes the console of the process.
230func (process *Process) ResizeConsole(ctx context.Context, width, height uint16) error {
231	process.handleLock.RLock()
232	defer process.handleLock.RUnlock()
233
234	operation := "hcsshim::Process::ResizeConsole"
235
236	if process.handle == 0 {
237		return makeProcessError(process, operation, ErrAlreadyClosed, nil)
238	}
239
240	modifyRequest := processModifyRequest{
241		Operation: modifyConsoleSize,
242		ConsoleSize: &consoleSize{
243			Height: height,
244			Width:  width,
245		},
246	}
247
248	modifyRequestb, err := json.Marshal(modifyRequest)
249	if err != nil {
250		return err
251	}
252
253	resultJSON, err := vmcompute.HcsModifyProcess(ctx, process.handle, string(modifyRequestb))
254	events := processHcsResult(ctx, resultJSON)
255	if err != nil {
256		return makeProcessError(process, operation, err, events)
257	}
258
259	return nil
260}
261
262// ExitCode returns the exit code of the process. The process must have
263// already terminated.
264func (process *Process) ExitCode() (int, error) {
265	select {
266	case <-process.waitBlock:
267		if process.waitError != nil {
268			return -1, process.waitError
269		}
270		return process.exitCode, nil
271	default:
272		return -1, makeProcessError(process, "hcsshim::Process::ExitCode", ErrInvalidProcessState, nil)
273	}
274}
275
276// StdioLegacy returns the stdin, stdout, and stderr pipes, respectively. Closing
277// these pipes does not close the underlying pipes. Once returned, these pipes
278// are the responsibility of the caller to close.
279func (process *Process) StdioLegacy() (_ io.WriteCloser, _ io.ReadCloser, _ io.ReadCloser, err error) {
280	operation := "hcsshim::Process::StdioLegacy"
281	ctx, span := trace.StartSpan(context.Background(), operation)
282	defer span.End()
283	defer func() { oc.SetSpanStatus(span, err) }()
284	span.AddAttributes(
285		trace.StringAttribute("cid", process.SystemID()),
286		trace.Int64Attribute("pid", int64(process.processID)))
287
288	process.handleLock.RLock()
289	defer process.handleLock.RUnlock()
290
291	if process.handle == 0 {
292		return nil, nil, nil, makeProcessError(process, operation, ErrAlreadyClosed, nil)
293	}
294
295	process.stdioLock.Lock()
296	defer process.stdioLock.Unlock()
297	if process.hasCachedStdio {
298		stdin, stdout, stderr := process.stdin, process.stdout, process.stderr
299		process.stdin, process.stdout, process.stderr = nil, nil, nil
300		process.hasCachedStdio = false
301		return stdin, stdout, stderr, nil
302	}
303
304	processInfo, resultJSON, err := vmcompute.HcsGetProcessInfo(ctx, process.handle)
305	events := processHcsResult(ctx, resultJSON)
306	if err != nil {
307		return nil, nil, nil, makeProcessError(process, operation, err, events)
308	}
309
310	pipes, err := makeOpenFiles([]syscall.Handle{processInfo.StdInput, processInfo.StdOutput, processInfo.StdError})
311	if err != nil {
312		return nil, nil, nil, makeProcessError(process, operation, err, nil)
313	}
314
315	return pipes[0], pipes[1], pipes[2], nil
316}
317
318// Stdio returns the stdin, stdout, and stderr pipes, respectively.
319// To close them, close the process handle.
320func (process *Process) Stdio() (stdin io.Writer, stdout, stderr io.Reader) {
321	process.stdioLock.Lock()
322	defer process.stdioLock.Unlock()
323	return process.stdin, process.stdout, process.stderr
324}
325
326// CloseStdin closes the write side of the stdin pipe so that the process is
327// notified on the read side that there is no more data in stdin.
328func (process *Process) CloseStdin(ctx context.Context) error {
329	process.handleLock.RLock()
330	defer process.handleLock.RUnlock()
331
332	operation := "hcsshim::Process::CloseStdin"
333
334	if process.handle == 0 {
335		return makeProcessError(process, operation, ErrAlreadyClosed, nil)
336	}
337
338	modifyRequest := processModifyRequest{
339		Operation: modifyCloseHandle,
340		CloseHandle: &closeHandle{
341			Handle: stdIn,
342		},
343	}
344
345	modifyRequestb, err := json.Marshal(modifyRequest)
346	if err != nil {
347		return err
348	}
349
350	resultJSON, err := vmcompute.HcsModifyProcess(ctx, process.handle, string(modifyRequestb))
351	events := processHcsResult(ctx, resultJSON)
352	if err != nil {
353		return makeProcessError(process, operation, err, events)
354	}
355
356	process.stdioLock.Lock()
357	if process.stdin != nil {
358		process.stdin.Close()
359		process.stdin = nil
360	}
361	process.stdioLock.Unlock()
362
363	return nil
364}
365
366// Close cleans up any state associated with the process but does not kill
367// or wait on it.
368func (process *Process) Close() (err error) {
369	operation := "hcsshim::Process::Close"
370	ctx, span := trace.StartSpan(context.Background(), operation)
371	defer span.End()
372	defer func() { oc.SetSpanStatus(span, err) }()
373	span.AddAttributes(
374		trace.StringAttribute("cid", process.SystemID()),
375		trace.Int64Attribute("pid", int64(process.processID)))
376
377	process.handleLock.Lock()
378	defer process.handleLock.Unlock()
379
380	// Don't double free this
381	if process.handle == 0 {
382		return nil
383	}
384
385	process.stdioLock.Lock()
386	if process.stdin != nil {
387		process.stdin.Close()
388		process.stdin = nil
389	}
390	if process.stdout != nil {
391		process.stdout.Close()
392		process.stdout = nil
393	}
394	if process.stderr != nil {
395		process.stderr.Close()
396		process.stderr = nil
397	}
398	process.stdioLock.Unlock()
399
400	if err = process.unregisterCallback(ctx); err != nil {
401		return makeProcessError(process, operation, err, nil)
402	}
403
404	if err = vmcompute.HcsCloseProcess(ctx, process.handle); err != nil {
405		return makeProcessError(process, operation, err, nil)
406	}
407
408	process.handle = 0
409	process.closedWaitOnce.Do(func() {
410		process.exitCode = -1
411		process.waitError = ErrAlreadyClosed
412		close(process.waitBlock)
413	})
414
415	return nil
416}
417
418func (process *Process) registerCallback(ctx context.Context) error {
419	callbackContext := &notifcationWatcherContext{
420		channels:  newProcessChannels(),
421		systemID:  process.SystemID(),
422		processID: process.processID,
423	}
424
425	callbackMapLock.Lock()
426	callbackNumber := nextCallback
427	nextCallback++
428	callbackMap[callbackNumber] = callbackContext
429	callbackMapLock.Unlock()
430
431	callbackHandle, err := vmcompute.HcsRegisterProcessCallback(ctx, process.handle, notificationWatcherCallback, callbackNumber)
432	if err != nil {
433		return err
434	}
435	callbackContext.handle = callbackHandle
436	process.callbackNumber = callbackNumber
437
438	return nil
439}
440
441func (process *Process) unregisterCallback(ctx context.Context) error {
442	callbackNumber := process.callbackNumber
443
444	callbackMapLock.RLock()
445	callbackContext := callbackMap[callbackNumber]
446	callbackMapLock.RUnlock()
447
448	if callbackContext == nil {
449		return nil
450	}
451
452	handle := callbackContext.handle
453
454	if handle == 0 {
455		return nil
456	}
457
458	// vmcompute.HcsUnregisterProcessCallback has its own synchronization to
459	// wait for all callbacks to complete. We must NOT hold the callbackMapLock.
460	err := vmcompute.HcsUnregisterProcessCallback(ctx, handle)
461	if err != nil {
462		return err
463	}
464
465	closeChannels(callbackContext.channels)
466
467	callbackMapLock.Lock()
468	delete(callbackMap, callbackNumber)
469	callbackMapLock.Unlock()
470
471	handle = 0
472
473	return nil
474}
475