Skip to content

Commit

Permalink
Add docs for terminals
Browse files Browse the repository at this point in the history
Signed-off-by: Avi Deitcher <[email protected]>
  • Loading branch information
deitch committed Feb 22, 2018
1 parent b50fa98 commit 4149ab5
Showing 1 changed file with 141 additions and 0 deletions.
141 changes: 141 additions & 0 deletions docs/terminals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Terminals and Standard IO
All processes on Unix (and Unix-like) operating systems have 3 standard file descriptors (fd) passed to them at start, collectively standard-IO (`stdio`):

* `0`: standard-in (`stdin`), the input stream into the process
* `1`: standard-out (`stdout`), the output stream from the process
* `2`: standard-error (`stderr`), the error stream from the process

When creating and running a container via `runc`, it is important to take care to structure the stdio the new container's process receives.

## Terminal Modes

`runc` supports two distinct methods for passing stdio to the container's primary process:

* pass-through
* new terminal

### Pass-Through
In pass-through mode, the `stdio` passed to the `runc` call will be passed as is to the primary container. This means that if you do the following:

```
$ echo input | runc run some_container > /tmp/log.out 2>& /tmp/log.err
```

Then the stdout of `some_container` will appear in the file `/tmp/log.out`, the stderr will appear in `/tmp/log.err`, and the process will receive input of `input`. `runc`'s stdout was redirected to `/tmp/log.out`, and `runc`, in turn, passed that handle to the container itself.

To invoke pass-through mode, configure your container's `config.json` with `terminal: false` (which is the default).

### New Terminal
If you do _not_ want to pass your current stdio through to the container, tell it to create a new terminal.

When you do so, `runc` will do the following:

1. Create a new pseudo-terminal (`pty`).
2. Pass this pty's slave to the container's primary process as its stdio.
3. Send the pty's master to a process to read/write stdio for the container's primary process (details below).

To invoke new terminal mode, configure your container's `config.json` with `terminal: true`.

## Which Mode To Use
`runc` itself runs in two modes:

* foreground
* detached

You can use either terminal mode with either runc mode. However, there are considerations that may indicate preference for one mode over another.

### Foreground
When running in foreground, `runc` and the container you invoke are most like running a regular foreground process. This is the scenario most likely to be useful for running in pass-through terminal mode. As the example above showed:

```
$ echo input | runc run some_container > /tmp/log.out 2>& /tmp/log.err
```

Nonetheless, you still have the option, when running in foreground runc mode, of using New Terminal mode. However, if the process is short-lived, you will not have much time to retrieve the console's fds from the socket and doing anything with it.

### Detached
When running in detached runc mode, you can run your terminal either in Pass-Through or New-Terminal mode.

You run in detached mode by doing one of the following:

* `runc -d`
* `runc create` followed by `runc start`, i.e. container lifecycle management

#### Pass-Through Detached
When running runc detached with terminal Pass-Through, the stdin/stdout/stderr of the calling process will be passed to `runc` and from there to the container's process. However, since `runc` detaches, there are several side effects to take into account:

* The stdout/stderr of the process can become intermixed with the stdout/stderr of the calling process.
* Any input into the calling process can cause indeterminate issues with which process actually receives the input.

For example:

```
$ runc create foo # container created, inheriting stdio from this terminal
$ runc start foo # container started, inheriting stdio from this terminal
$ curl https://containerd.io # get contents of this site to the terminal
<!DOCTYPE html>
I am container output
<html lang="en">
<head>
ASashcned more container output
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>containerd</title>
```

Two problems occurred above:

1. The output from the `curl` command and the output of the container `foo` became intermingled.
2. Our typing `curl https://containerd.io` actually was passed to both our shell and the container `foo`. The results can be unpredictable. Our example here was simulated and might not even work.

Further, a third, possibly temporary problem exists. Pass-Through Detached only works correctly when stdout/stderr are either an actual terminal or a file. If you use _anything_ else, e.g. a pipe, it will hang, for reasons unknown. There is an [open issue](https://github.com/opencontainers/runc/issues/1721) on this.

For this reason, we **strongly recommend** using New Terminal mode when running `runc` detached.

#### New Terminal Detached
When running runc detached with New Terminal mode, runc behaves exactly as described above, creating a new pty, passing it to the container, sending the fd to a socket, and exiting.

## How To Get the FD for New Terminal
When running `runc` in New Terminal mode, i.e. `terminal:true` in `config.json` - whether foreground or detached `runc` - you **must** tell `runc` what process will control the master end of the pty. To do that:

1. Open a Unix-domain socket
2. Pass the path to the socket to `runc create <container> --console-socket <path_to_socket>`
3. Listen on the socket to get the master file descriptor
4. Use that file descriptor to control stdio

Note the one shortcoming: the single descriptor means that you have mixed stdout/stderr. There is no way to separate them. Essentially, this is a _console_, which has no separate stdout and stderr.

The following sample code, partially taken from the reference implementation [recvtty](https://github.com/opencontainers/runc/blob/master/contrib/cmd/recvtty/recvtty.go), shows how to accept a file descriptor for the socket. It does not contain the usual `err` handling and `defer`red closing.

```go
// only partial imports listed
import (
"github.com/containerd/console"
"github.com/opencontainers/runc/libcontainer/utils"
)

// path is path to a Unix domain socket
ln, err := net.Listen("unix", path)
conn, err := ln.Accept()
unixconn, ok := conn.(*net.UnixConn)
socket, err := unixconn.File()

master, err := utils.RecvFd(socket)
c, err := console.ConsoleFromFile(master)

// Copy from our stdio to the master fd.
quitChan := make(chan struct{})
go func() {
io.Copy(os.Stdout, c)
quitChan <- struct{}{}
}()
go func() {
io.Copy(c, os.Stdin)
quitChan <- struct{}{}
}()

// Only close the master fd once we've stopped copying.
<-quitChan
c.Close()
```

0 comments on commit 4149ab5

Please sign in to comment.