Skip to content

Commit 1543444

Browse files
committed
contrib: add recvtty proof-of-concept
This is a proof-of-concept for the --console-socket API. It just acts as a dumb input-output copy process (nowhere near as good as the internal runC one since it doesn't handle console resizes or signals). It also provides a test-friendly mode that will be used in the bats integration tests. This patch is part of the console rewrite patchset. Signed-off-by: Aleksa Sarai <[email protected]>
1 parent 7df64f8 commit 1543444

File tree

3 files changed

+265
-5
lines changed

3 files changed

+265
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
vendor/pkg
22
/runc
3+
contrib/cmd/recvtty/recvtty
34
Godeps/_workspace/src/github.com/opencontainers/runc
45
man/man8
56
release

Makefile

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
.PHONY: dbuild man \
1+
.PHONY: all dbuild man \
22
localtest localunittest localintegration \
33
test unittest integration
44

5+
SOURCES := $(shell find . 2>&1 | grep -E '.*\.(c|h|go)$$')
56
PREFIX := $(DESTDIR)/usr/local
67
BINDIR := $(PREFIX)/sbin
78
GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null)
@@ -26,13 +27,23 @@ VERSION := ${shell cat ./VERSION}
2627

2728
SHELL := $(shell command -v bash 2>/dev/null)
2829

29-
all: $(RUNC_LINK)
30+
.DEFAULT: runc
31+
32+
runc: $(SOURCES) | $(RUNC_LINK)
3033
go build -i -ldflags "-X main.gitCommit=${COMMIT} -X main.version=${VERSION}" -tags "$(BUILDTAGS)" -o runc .
3134

32-
static: $(RUNC_LINK)
35+
all: runc recvtty
36+
37+
recvtty: contrib/cmd/recvtty/recvtty
38+
39+
contrib/cmd/recvtty/recvtty: $(SOURCES) | $(RUNC_LINK)
40+
go build -i -ldflags "-X main.gitCommit=${COMMIT} -X main.version=${VERSION}" -tags "$(BUILDTAGS)" -o contrib/cmd/recvtty/recvtty ./contrib/cmd/recvtty
41+
42+
static: $(SOURCES) | $(RUNC_LINK)
3343
CGO_ENABLED=1 go build -i -tags "$(BUILDTAGS) cgo static_build" -ldflags "-w -extldflags -static -X main.gitCommit=${COMMIT} -X main.version=${VERSION}" -o runc .
44+
CGO_ENABLED=1 go build -i -tags "$(BUILDTAGS) cgo static_build" -ldflags "-w -extldflags -static -X main.gitCommit=${COMMIT} -X main.version=${VERSION}" -o contrib/cmd/recvtty/recvtty ./contrib/cmd/recvtty
3445

35-
release: $(RUNC_LINK)
46+
release: $(RUNC_LINK) | $(RUNC_LINK)
3647
@flag_list=(seccomp selinux apparmor static ambient); \
3748
unset expression; \
3849
for flag in "$${flag_list[@]}"; do \
@@ -62,7 +73,7 @@ $(RUNC_LINK):
6273
ln -sfn $(CURDIR) $(RUNC_LINK)
6374

6475
dbuild: runcimage
65-
docker run --rm -v $(CURDIR):/go/src/$(PROJECT) --privileged $(RUNC_IMAGE) make
76+
docker run --rm -v $(CURDIR):/go/src/$(PROJECT) --privileged $(RUNC_IMAGE) make clean all
6677

6778
lint:
6879
go vet ./...
@@ -113,6 +124,7 @@ uninstall-man:
113124

114125
clean:
115126
rm -f runc
127+
rm -f contrib/cmd/recvtty/recvtty
116128
rm -f $(RUNC_LINK)
117129
rm -rf $(GOPATH)/pkg
118130
rm -rf $(RELEASE_DIR)

contrib/cmd/recvtty/recvtty.go

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/*
2+
* Copyright 2016 SUSE LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"fmt"
21+
"io"
22+
"io/ioutil"
23+
"net"
24+
"os"
25+
"strings"
26+
27+
"github.com/opencontainers/runc/libcontainer"
28+
"github.com/opencontainers/runc/libcontainer/utils"
29+
"github.com/urfave/cli"
30+
)
31+
32+
// version will be populated by the Makefile, read from
33+
// VERSION file of the source code.
34+
var version = ""
35+
36+
// gitCommit will be the hash that the binary was built from
37+
// and will be populated by the Makefile
38+
var gitCommit = ""
39+
40+
const (
41+
usage = `Open Container Initiative contrib/cmd/recvtty
42+
43+
recvtty is a reference implementation of a consumer of runC's --console-socket
44+
API. It has two main modes of operation:
45+
46+
* single: Only permit one terminal to be sent to the socket, which is
47+
then hooked up to the stdio of the recvtty process. This is useful
48+
for rudimentary shell management of a container.
49+
50+
* null: Permit as many terminals to be sent to the socket, but they
51+
are read to /dev/null. This is used for testing, and imitates the
52+
old runC API's --console=/dev/pts/ptmx hack which would allow for a
53+
similar trick. This is probably not what you want to use, unless
54+
you're doing something like our bats integration tests.
55+
56+
To use recvtty, just specify a socket path at which you want to receive
57+
terminals:
58+
59+
$ recvtty [--mode <single|null>] socket.sock
60+
`
61+
)
62+
63+
func bail(err error) {
64+
fmt.Fprintf(os.Stderr, "[recvtty] fatal error: %v\n", err)
65+
os.Exit(1)
66+
}
67+
68+
func handleSingle(path string) error {
69+
// Open a socket.
70+
ln, err := net.Listen("unix", path)
71+
if err != nil {
72+
return err
73+
}
74+
defer ln.Close()
75+
76+
// We only accept a single connection, since we can only really have
77+
// one reader for os.Stdin. Plus this is all a PoC.
78+
conn, err := ln.Accept()
79+
if err != nil {
80+
return err
81+
}
82+
defer conn.Close()
83+
84+
// Close ln, to allow for other instances to take over.
85+
ln.Close()
86+
87+
// Get the fd of the connection.
88+
unixconn, ok := conn.(*net.UnixConn)
89+
if !ok {
90+
return fmt.Errorf("failed to cast to unixconn")
91+
}
92+
93+
socket, err := unixconn.File()
94+
if err != nil {
95+
return err
96+
}
97+
defer socket.Close()
98+
99+
// Get the master file descriptor from runC.
100+
master, err := utils.RecvFd(socket)
101+
if err != nil {
102+
return err
103+
}
104+
105+
// Print the file descriptor tag.
106+
ti, err := libcontainer.GetTerminalInfo(master.Name())
107+
if err != nil {
108+
return err
109+
}
110+
fmt.Printf("[recvtty] received masterfd: container '%s'\n", ti.ContainerID)
111+
112+
// Copy from our stdio to the master fd.
113+
quitChan := make(chan struct{})
114+
go func() {
115+
io.Copy(os.Stdout, master)
116+
quitChan <- struct{}{}
117+
}()
118+
go func() {
119+
io.Copy(master, os.Stdin)
120+
quitChan <- struct{}{}
121+
}()
122+
123+
// Only close the master fd once we've stopped copying.
124+
<-quitChan
125+
master.Close()
126+
return nil
127+
}
128+
129+
func handleNull(path string) error {
130+
// Open a socket.
131+
ln, err := net.Listen("unix", path)
132+
if err != nil {
133+
return err
134+
}
135+
defer ln.Close()
136+
137+
// As opposed to handleSingle we accept as many connections as we get, but
138+
// we don't interact with Stdin at all (and we copy stdout to /dev/null).
139+
for {
140+
conn, err := ln.Accept()
141+
if err != nil {
142+
return err
143+
}
144+
go func(conn net.Conn) {
145+
// Don't leave references lying around.
146+
defer conn.Close()
147+
148+
// Get the fd of the connection.
149+
unixconn, ok := conn.(*net.UnixConn)
150+
if !ok {
151+
return
152+
}
153+
154+
socket, err := unixconn.File()
155+
if err != nil {
156+
return
157+
}
158+
defer socket.Close()
159+
160+
// Get the master file descriptor from runC.
161+
master, err := utils.RecvFd(socket)
162+
if err != nil {
163+
return
164+
}
165+
166+
// Print the file descriptor tag.
167+
ti, err := libcontainer.GetTerminalInfo(master.Name())
168+
if err != nil {
169+
bail(err)
170+
}
171+
fmt.Printf("[recvtty] received masterfd: container '%s'\n", ti.ContainerID)
172+
173+
// Just do a dumb copy to /dev/null.
174+
devnull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
175+
if err != nil {
176+
// TODO: Handle this nicely.
177+
return
178+
}
179+
180+
io.Copy(devnull, master)
181+
devnull.Close()
182+
}(conn)
183+
}
184+
}
185+
186+
func main() {
187+
app := cli.NewApp()
188+
app.Name = "recvtty"
189+
app.Usage = usage
190+
191+
// Set version to be the same as runC.
192+
var v []string
193+
if version != "" {
194+
v = append(v, version)
195+
}
196+
if gitCommit != "" {
197+
v = append(v, fmt.Sprintf("commit: %s", gitCommit))
198+
}
199+
app.Version = strings.Join(v, "\n")
200+
201+
// Set the flags.
202+
app.Flags = []cli.Flag{
203+
cli.StringFlag{
204+
Name: "mode, m",
205+
Value: "single",
206+
Usage: "Mode of operation (single or null)",
207+
},
208+
cli.StringFlag{
209+
Name: "pid-file",
210+
Value: "",
211+
Usage: "Path to write daemon process ID to",
212+
},
213+
}
214+
215+
app.Action = func(ctx *cli.Context) error {
216+
args := ctx.Args()
217+
if len(args) != 1 {
218+
return fmt.Errorf("need to specify a single socket path")
219+
}
220+
path := ctx.Args()[0]
221+
222+
pidPath := ctx.String("pid-file")
223+
if pidPath != "" {
224+
pid := fmt.Sprintf("%d\n", os.Getpid())
225+
if err := ioutil.WriteFile(pidPath, []byte(pid), 0644); err != nil {
226+
return err
227+
}
228+
}
229+
230+
switch ctx.String("mode") {
231+
case "single":
232+
if err := handleSingle(path); err != nil {
233+
return err
234+
}
235+
case "null":
236+
if err := handleNull(path); err != nil {
237+
return err
238+
}
239+
default:
240+
return fmt.Errorf("need to select a valid mode: %s", ctx.String("mode"))
241+
}
242+
return nil
243+
}
244+
if err := app.Run(os.Args); err != nil {
245+
bail(err)
246+
}
247+
}

0 commit comments

Comments
 (0)