Skip to content

Commit b54460a

Browse files
authored
Merge pull request #292 from djs55/improve-serial-console
go: improve serial console
2 parents 163c134 + 04d209c commit b54460a

File tree

5 files changed

+214
-28
lines changed

5 files changed

+214
-28
lines changed

go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module github.com/moby/hyperkit
2+
3+
go 1.14
4+
5+
require (
6+
github.com/mitchellh/go-ps v1.0.0
7+
github.com/stretchr/testify v1.6.1
8+
golang.org/x/sys v0.0.0-20200722175500-76b94024e4b6
9+
)

go.sum

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
4+
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
5+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
8+
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
9+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
10+
golang.org/x/sys v0.0.0-20200722175500-76b94024e4b6 h1:X9xIZ1YU8bLZA3l6gqDUHSFiD0GFI9S548h6C8nDtOY=
11+
golang.org/x/sys v0.0.0-20200722175500-76b94024e4b6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
12+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
13+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
14+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

go/hyperkit.go

Lines changed: 138 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"os"
3232
"os/exec"
3333
"os/user"
34+
"path"
3435
"path/filepath"
3536
"strconv"
3637
"strings"
@@ -40,11 +41,11 @@ import (
4041
)
4142

4243
const (
43-
// ConsoleStdio configures console to use Stdio
44+
// ConsoleStdio configures console to use Stdio (deprecated)
4445
ConsoleStdio = iota
45-
// ConsoleFile configures console to a tty and output to a file
46+
// ConsoleFile configures console to a tty and output to a file (deprecated)
4647
ConsoleFile
47-
// ConsoleLog configures console to a tty and sends its contents to the logs
48+
// ConsoleLog configures console to a tty and sends its contents to the logs (deprecated)
4849
ConsoleLog
4950

5051
legacyVPNKitSock = "Library/Containers/com.docker.docker/Data/s50"
@@ -120,9 +121,13 @@ type HyperKit struct {
120121
// Memory is the amount of megabytes of memory for the VM.
121122
Memory int `json:"memory"`
122123

123-
// Console defines where the console of the VM should be connected to.
124+
// Console defines where the console of the VM should be connected to. (deprecated)
124125
Console int `json:"console"`
125126

127+
// Serials defines what happens to the I/O on the serial ports. If this is not nil
128+
// it overrides the Console setting.
129+
Serials []Serial `json:"serials"`
130+
126131
// Below here are internal members, but they are exported so
127132
// that they are written to the state json file, if configured.
128133

@@ -136,6 +141,28 @@ type HyperKit struct {
136141
process *os.Process
137142
}
138143

144+
// Serial port.
145+
type Serial struct {
146+
// InteractiveConsole allows a user to connect to a live VM serial console.
147+
InteractiveConsole InteractiveConsole
148+
// LogToRingBuffer will write console output to a fixed size ring buffer file.
149+
LogToRingBuffer bool
150+
// LogToASL will write console output to the Apple System Log.
151+
LogToASL bool
152+
}
153+
154+
// InteractiveConsole is an optional interactive VM console.
155+
type InteractiveConsole int
156+
157+
const (
158+
// NoInteractiveConsole disables the interactive console.
159+
NoInteractiveConsole = InteractiveConsole(iota)
160+
// StdioInteractiveConsole creates a console on stdio.
161+
StdioInteractiveConsole
162+
// TTYInteractiveConsole creates a console on a TTY.
163+
TTYInteractiveConsole
164+
)
165+
139166
// New creates a template config structure.
140167
// - If hyperkit can't be found an error is returned.
141168
// - If vpnkitsock is empty no networking is configured. If it is set
@@ -196,11 +223,7 @@ func (h *HyperKit) Start(cmdline string) (chan error, error) {
196223
return errCh, nil
197224
}
198225

199-
// check validates `h`. It also creates the disks if needed.
200-
func (h *HyperKit) check() error {
201-
log.Debugf("hyperkit: check %#v", h)
202-
var err error
203-
// Sanity checks on configuration
226+
func (h *HyperKit) checkLegacyConsole() error {
204227
switch h.Console {
205228
case ConsoleFile, ConsoleLog:
206229
if h.StateDir == "" {
@@ -211,6 +234,48 @@ func (h *HyperKit) check() error {
211234
return fmt.Errorf("If ConsoleStdio is set but stdio is not a terminal, StateDir must be specified")
212235
}
213236
}
237+
return nil
238+
}
239+
240+
func (h *HyperKit) checkSerials() error {
241+
stdioConsole := -1
242+
for i, serial := range h.Serials {
243+
if serial.LogToRingBuffer && h.StateDir == "" {
244+
return fmt.Errorf("If VM is to log to a ring buffer, StateDir must be specified")
245+
}
246+
if serial.InteractiveConsole == StdioInteractiveConsole {
247+
if isTerminal(os.Stdout) {
248+
return fmt.Errorf("If StdioInteractiveConsole is set, stdio must be a TTY")
249+
}
250+
if stdioConsole != -1 {
251+
return fmt.Errorf("Only one serial port can be nominated as the stdio interactive console")
252+
}
253+
stdioConsole = i
254+
}
255+
if serial.InteractiveConsole == TTYInteractiveConsole && h.StateDir == "" {
256+
return fmt.Errorf("If TTYInteractiveConsole is set, StateDir must be specified ")
257+
}
258+
if serial.LogToRingBuffer && h.StateDir == "" {
259+
return fmt.Errorf("If LogToRingBuffer is set, StateDir must be specified")
260+
}
261+
}
262+
return nil
263+
}
264+
265+
// check validates `h`. It also creates the disks if needed.
266+
func (h *HyperKit) check() error {
267+
log.Debugf("hyperkit: check %#v", h)
268+
var err error
269+
// Sanity checks on configuration
270+
if h.Serials == nil {
271+
if err := h.checkLegacyConsole(); err != nil {
272+
return err
273+
}
274+
} else {
275+
if err := h.checkSerials(); err != nil {
276+
return err
277+
}
278+
}
214279
for _, image := range h.ISOImages {
215280
if _, err = os.Stat(image); os.IsNotExist(err) {
216281
return fmt.Errorf("ISO %s does not exist", image)
@@ -365,6 +430,44 @@ func intArrayToString(i []int, sep string) string {
365430
return strings.Join(s, sep)
366431
}
367432

433+
func (h *HyperKit) legacyConsoleArgs() []string {
434+
cfg := "com1"
435+
if h.Console == ConsoleStdio && isTerminal(os.Stdout) {
436+
cfg += fmt.Sprintf(",stdio")
437+
} else {
438+
cfg += fmt.Sprintf(",autopty=%s/tty", h.StateDir)
439+
}
440+
if h.Console == ConsoleLog {
441+
cfg += fmt.Sprintf(",asl")
442+
} else {
443+
cfg += fmt.Sprintf(",log=%s/console-ring", h.StateDir)
444+
}
445+
return []string{"-l", cfg}
446+
}
447+
448+
func (h *HyperKit) serialArgs() []string {
449+
results := []string{}
450+
for i, serial := range h.Serials {
451+
cfg := fmt.Sprintf("com%d", i+1)
452+
switch serial.InteractiveConsole {
453+
case NoInteractiveConsole:
454+
cfg += ",null"
455+
case StdioInteractiveConsole:
456+
cfg += fmt.Sprintf(",stdio")
457+
case TTYInteractiveConsole:
458+
cfg += fmt.Sprintf(",autopty=%s/tty%d", h.StateDir, i+1)
459+
}
460+
if serial.LogToASL {
461+
cfg += fmt.Sprintf(",asl")
462+
}
463+
if serial.LogToRingBuffer {
464+
cfg += fmt.Sprintf(",log=%s/console-ring", h.StateDir)
465+
}
466+
results = append(results, "-l", cfg)
467+
}
468+
return results
469+
}
470+
368471
func (h *HyperKit) buildArgs(cmdline string) {
369472
a := []string{"-A", "-u"}
370473
if h.StateDir != "" {
@@ -429,19 +532,10 @@ func (h *HyperKit) buildArgs(cmdline string) {
429532
}
430533

431534
// -l: LPC device configuration.
432-
{
433-
cfg := "com1"
434-
if h.Console == ConsoleStdio && isTerminal(os.Stdout) {
435-
cfg += fmt.Sprintf(",stdio")
436-
} else {
437-
cfg += fmt.Sprintf(",autopty=%s/tty", h.StateDir)
438-
}
439-
if h.Console == ConsoleLog {
440-
cfg += fmt.Sprintf(",asl")
441-
} else {
442-
cfg += fmt.Sprintf(",log=%s/console-ring", h.StateDir)
443-
}
444-
a = append(a, "-l", cfg)
535+
if h.Serials == nil {
536+
a = append(a, h.legacyConsoleArgs()...)
537+
} else {
538+
a = append(a, h.serialArgs()...)
445539
}
446540

447541
if h.Bootrom == "" {
@@ -459,8 +553,8 @@ func (h *HyperKit) buildArgs(cmdline string) {
459553
}
460554

461555
// openTTY opens the tty files for reading, and returns it.
462-
func (h *HyperKit) openTTY() *os.File {
463-
path := fmt.Sprintf("%s/tty", h.StateDir)
556+
func (h *HyperKit) openTTY(filename string) *os.File {
557+
path := path.Join(h.StateDir, filename)
464558
for {
465559
if res, err := os.OpenFile(path, os.O_RDONLY, 0); err != nil {
466560
log.Infof("hyperkit: openTTY: %v, retrying", err)
@@ -474,6 +568,21 @@ func (h *HyperKit) openTTY() *os.File {
474568
}
475569
}
476570

571+
func (h *HyperKit) findStdioTTY() string {
572+
if h.Serials != nil {
573+
for i, serial := range h.Serials {
574+
if serial.InteractiveConsole == StdioInteractiveConsole {
575+
return fmt.Sprintf("tty%d", i)
576+
}
577+
}
578+
return ""
579+
}
580+
if h.Console == ConsoleStdio {
581+
return "tty"
582+
}
583+
return ""
584+
}
585+
477586
// execute forges the command to run hyperkit, runs and returns it.
478587
// It also plumbs stdin/stdout/stderr.
479588
func (h *HyperKit) execute() (*exec.Cmd, error) {
@@ -496,19 +605,21 @@ func (h *HyperKit) execute() (*exec.Cmd, error) {
496605
//
497606
// If a logger is specified, use it for stdout/stderr
498607
// logging. Otherwise use the default /dev/null.
499-
if h.Console == ConsoleStdio {
608+
filename := h.findStdioTTY()
609+
if filename != "" {
500610
if isTerminal(os.Stdout) {
501611
cmd.Stdin = os.Stdin
502612
cmd.Stdout = os.Stdout
503613
cmd.Stderr = os.Stderr
504614
} else {
505615
go func() {
506-
tty := h.openTTY()
616+
tty := h.openTTY(filename)
507617
defer tty.Close()
508618
io.Copy(os.Stdout, tty)
509619
}()
510620
}
511-
} else if log != nil {
621+
}
622+
if log != nil {
512623
log.Debugf("hyperkit: Redirecting stdout/stderr to logger")
513624
stdout, err := cmd.StdoutPipe()
514625
if err != nil {

go/hyperkit_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package hyperkit
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestLegacyConsole(t *testing.T) {
11+
h, err := New("sh", "", "state-dir")
12+
require.Nil(t, err)
13+
14+
h.Console = ConsoleFile
15+
h.buildArgs("")
16+
assert.EqualValues(t, []string{"-A", "-u", "-F", "state-dir/hyperkit.pid", "-c", "1", "-m", "1024M", "-s", "0:0,hostbridge", "-s", "31,lpc", "-s", "1,virtio-rnd", "-l", "com1,autopty=state-dir/tty,log=state-dir/console-ring", "-f", "kexec,,,earlyprintk=serial "}, h.Arguments)
17+
}
18+
19+
func TestNewSerial(t *testing.T) {
20+
h, err := New("sh", "", "state-dir")
21+
require.Nil(t, err)
22+
23+
h.Serials = []Serial{
24+
{
25+
InteractiveConsole: TTYInteractiveConsole,
26+
LogToRingBuffer: true,
27+
},
28+
}
29+
h.buildArgs("")
30+
assert.EqualValues(t, []string{"-A", "-u", "-F", "state-dir/hyperkit.pid", "-c", "1", "-m", "1024M", "-s", "0:0,hostbridge", "-s", "31,lpc", "-s", "1,virtio-rnd", "-l", "com1,autopty=state-dir/tty1,log=state-dir/console-ring", "-f", "kexec,,,earlyprintk=serial "}, h.Arguments)
31+
}
32+
33+
func TestNullSerial(t *testing.T) {
34+
h, err := New("sh", "", "state-dir")
35+
require.Nil(t, err)
36+
37+
h.Serials = []Serial{
38+
{
39+
LogToRingBuffer: true,
40+
},
41+
}
42+
h.buildArgs("")
43+
assert.EqualValues(t, []string{"-A", "-u", "-F", "state-dir/hyperkit.pid", "-c", "1", "-m", "1024M", "-s", "0:0,hostbridge", "-s", "31,lpc", "-s", "1,virtio-rnd", "-l", "com1,null,log=state-dir/console-ring", "-f", "kexec,,,earlyprintk=serial "}, h.Arguments)
44+
}

src/lib/uart_emul.c

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,15 @@ uart_set_backend(struct uart_softc *sc, const char *backend, const char *devname
722722
if (next)
723723
next[0] = '\0';
724724

725-
if (strcmp("stdio", backend) == 0 && !uart_stdio) {
725+
if (strcmp("null", backend) == 0) {
726+
sc->tty.fd = open("/dev/null", O_RDWR | O_NONBLOCK);
727+
if (sc->tty.fd == -1) {
728+
fprintf(stderr, "error opening /dev/null\n");
729+
goto err;
730+
}
731+
sc->tty.opened = true;
732+
retval = 0;
733+
} else if (strcmp("stdio", backend) == 0 && !uart_stdio) {
726734
sc->tty.fd = STDIN_FILENO;
727735
sc->tty.opened = true;
728736
uart_stdio = true;

0 commit comments

Comments
 (0)