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