Implement TTY field in control.Processes().

Threadgroups already know their TTY (if they have one), which now contains the
TTY Index, and is returned in the Processes() call.

PiperOrigin-RevId: 284263850
This commit is contained in:
Nicolas Lacasse
2019-12-06 14:34:13 -08:00
committed by gVisor bot
parent ea7a100202
commit 663fe840f7
7 changed files with 173 additions and 14 deletions
+19 -7
View File
@@ -268,7 +268,6 @@ func (proc *Proc) Ps(args *PsArgs, out *string) error {
}
// Process contains information about a single process in a Sandbox.
// TODO(b/117881927): Implement TTY field.
type Process struct {
UID auth.KUID `json:"uid"`
PID kernel.ThreadID `json:"pid"`
@@ -276,6 +275,9 @@ type Process struct {
PPID kernel.ThreadID `json:"ppid"`
// Processor utilization
C int32 `json:"c"`
// TTY name of the process. Will be of the form "pts/N" if there is a
// TTY, or "?" if there is not.
TTY string `json:"tty"`
// Start time
STime string `json:"stime"`
// CPU time
@@ -285,18 +287,19 @@ type Process struct {
}
// ProcessListToTable prints a table with the following format:
// UID PID PPID C STIME TIME CMD
// 0 1 0 0 14:04 505262ns tail
// UID PID PPID C TTY STIME TIME CMD
// 0 1 0 0 pty/4 14:04 505262ns tail
func ProcessListToTable(pl []*Process) string {
var buf bytes.Buffer
tw := tabwriter.NewWriter(&buf, 10, 1, 3, ' ', 0)
fmt.Fprint(tw, "UID\tPID\tPPID\tC\tSTIME\tTIME\tCMD")
fmt.Fprint(tw, "UID\tPID\tPPID\tC\tTTY\tSTIME\tTIME\tCMD")
for _, d := range pl {
fmt.Fprintf(tw, "\n%d\t%d\t%d\t%d\t%s\t%s\t%s",
fmt.Fprintf(tw, "\n%d\t%d\t%d\t%d\t%s\t%s\t%s\t%s",
d.UID,
d.PID,
d.PPID,
d.C,
d.TTY,
d.STime,
d.Time,
d.Cmd)
@@ -347,7 +350,7 @@ func Processes(k *kernel.Kernel, containerID string, out *[]*Process) error {
if p := tg.Leader().Parent(); p != nil {
ppid = p.PIDNamespace().IDOfThreadGroup(p.ThreadGroup())
}
*out = append(*out, &Process{
p := Process{
UID: tg.Leader().Credentials().EffectiveKUID,
PID: pid,
PPID: ppid,
@@ -355,7 +358,9 @@ func Processes(k *kernel.Kernel, containerID string, out *[]*Process) error {
C: percentCPU(tg.CPUStats(), tg.Leader().StartTime(), now),
Time: tg.CPUStats().SysTime.String(),
Cmd: tg.Leader().Name(),
})
TTY: ttyName(tg.TTY()),
}
*out = append(*out, &p)
}
sort.Slice(*out, func(i, j int) bool { return (*out)[i].PID < (*out)[j].PID })
return nil
@@ -395,3 +400,10 @@ func percentCPU(stats usage.CPUStats, startTime, now ktime.Time) int32 {
}
return int32(percentCPU)
}
func ttyName(tty *kernel.TTY) string {
if tty == nil {
return "?"
}
return fmt.Sprintf("pts/%d", tty.Index)
}
+6 -4
View File
@@ -34,7 +34,7 @@ func TestProcessListTable(t *testing.T) {
}{
{
pl: []*Process{},
expected: "UID PID PPID C STIME TIME CMD",
expected: "UID PID PPID C TTY STIME TIME CMD",
},
{
pl: []*Process{
@@ -43,6 +43,7 @@ func TestProcessListTable(t *testing.T) {
PID: 0,
PPID: 0,
C: 0,
TTY: "?",
STime: "0",
Time: "0",
Cmd: "zero",
@@ -52,14 +53,15 @@ func TestProcessListTable(t *testing.T) {
PID: 1,
PPID: 1,
C: 1,
TTY: "pts/4",
STime: "1",
Time: "1",
Cmd: "one",
},
},
expected: `UID PID PPID C STIME TIME CMD
0 0 0 0 0 0 zero
1 1 1 1 1 1 one`,
expected: `UID PID PPID C TTY STIME TIME CMD
0 0 0 0 ? 0 0 zero
1 1 1 1 pts/4 1 1 one`,
},
}
+2 -2
View File
@@ -53,8 +53,8 @@ func newTerminal(ctx context.Context, d *dirInodeOperations, n uint32) *Terminal
d: d,
n: n,
ld: newLineDiscipline(termios),
masterKTTY: &kernel.TTY{},
slaveKTTY: &kernel.TTY{},
masterKTTY: &kernel.TTY{Index: n},
slaveKTTY: &kernel.TTY{Index: n},
}
t.EnableLeakCheck("tty.Terminal")
return &t
+11
View File
@@ -21,8 +21,19 @@ import "sync"
//
// +stateify savable
type TTY struct {
// Index is the terminal index. It is immutable.
Index uint32
mu sync.Mutex `state:"nosave"`
// tg is protected by mu.
tg *ThreadGroup
}
// TTY returns the thread group's controlling terminal. If nil, there is no
// controlling terminal.
func (tg *ThreadGroup) TTY() *TTY {
tg.signalHandlers.mu.Lock()
defer tg.signalHandlers.mu.Unlock()
return tg.tty
}
+94 -1
View File
@@ -98,10 +98,14 @@ func procListsEqual(got, want []*control.Process) bool {
for i := range got {
pd1 := got[i]
pd2 := want[i]
// Zero out unimplemented and timing dependant fields.
// Zero out timing dependant fields.
pd1.Time = ""
pd1.STime = ""
pd1.C = 0
// Ignore TTY field too, since it's not relevant in the cases
// where we use this method. Tests that care about the TTY
// field should check for it themselves.
pd1.TTY = ""
if *pd1 != *pd2 {
return false
}
@@ -2112,6 +2116,95 @@ func TestOverlayfsStaleRead(t *testing.T) {
}
}
// TestTTYField checks TTY field returned by container.Processes().
func TestTTYField(t *testing.T) {
stop := testutil.StartReaper()
defer stop()
testApp, err := testutil.FindFile("runsc/container/test_app/test_app")
if err != nil {
t.Fatal("error finding test_app:", err)
}
testCases := []struct {
name string
useTTY bool
wantTTYField string
}{
{
name: "no tty",
useTTY: false,
wantTTYField: "?",
},
{
name: "tty used",
useTTY: true,
wantTTYField: "pts/0",
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
conf := testutil.TestConfig()
// We will run /bin/sleep, possibly with an open TTY.
cmd := []string{"/bin/sleep", "10000"}
if test.useTTY {
// Run inside the "pty-runner".
cmd = append([]string{testApp, "pty-runner"}, cmd...)
}
spec := testutil.NewSpecWithArgs(cmd...)
rootDir, bundleDir, err := testutil.SetupContainer(spec, conf)
if err != nil {
t.Fatalf("error setting up container: %v", err)
}
defer os.RemoveAll(rootDir)
defer os.RemoveAll(bundleDir)
// Create and start the container.
args := Args{
ID: testutil.UniqueContainerID(),
Spec: spec,
BundleDir: bundleDir,
}
c, err := New(conf, args)
if err != nil {
t.Fatalf("error creating container: %v", err)
}
defer c.Destroy()
if err := c.Start(conf); err != nil {
t.Fatalf("error starting container: %v", err)
}
// Wait for sleep to be running, and check the TTY
// field.
var gotTTYField string
cb := func() error {
ps, err := c.Processes()
if err != nil {
err = fmt.Errorf("error getting process data from container: %v", err)
return &backoff.PermanentError{Err: err}
}
for _, p := range ps {
if strings.Contains(p.Cmd, "sleep") {
gotTTYField = p.TTY
return nil
}
}
return fmt.Errorf("sleep not running")
}
if err := testutil.Poll(cb, 30*time.Second); err != nil {
t.Fatalf("error waiting for sleep process: %v", err)
}
if gotTTYField != test.wantTTYField {
t.Errorf("tty field got %q, want %q", gotTTYField, test.wantTTYField)
}
})
}
}
// executeSync synchronously executes a new process.
func (cont *Container) executeSync(args *control.ExecArgs) (syscall.WaitStatus, error) {
pid, err := cont.Execute(args)
+1
View File
@@ -15,5 +15,6 @@ go_binary(
"//pkg/unet",
"//runsc/testutil",
"@com_github_google_subcommands//:go_default_library",
"@com_github_kr_pty//:go_default_library",
],
)
+40
View File
@@ -19,6 +19,7 @@ package main
import (
"context"
"fmt"
"io"
"io/ioutil"
"log"
"net"
@@ -31,6 +32,7 @@ import (
"flag"
"github.com/google/subcommands"
"github.com/kr/pty"
"gvisor.dev/gvisor/runsc/testutil"
)
@@ -41,6 +43,7 @@ func main() {
subcommands.Register(new(fdReceiver), "")
subcommands.Register(new(fdSender), "")
subcommands.Register(new(forkBomb), "")
subcommands.Register(new(ptyRunner), "")
subcommands.Register(new(reaper), "")
subcommands.Register(new(syscall), "")
subcommands.Register(new(taskTree), "")
@@ -352,3 +355,40 @@ func (c *capability) Execute(ctx context.Context, f *flag.FlagSet, args ...inter
return subcommands.ExitSuccess
}
type ptyRunner struct{}
// Name implements subcommands.Command.
func (*ptyRunner) Name() string {
return "pty-runner"
}
// Synopsis implements subcommands.Command.
func (*ptyRunner) Synopsis() string {
return "runs the given command with an open pty terminal"
}
// Usage implements subcommands.Command.
func (*ptyRunner) Usage() string {
return "pty-runner [command]"
}
// SetFlags implements subcommands.Command.SetFlags.
func (*ptyRunner) SetFlags(f *flag.FlagSet) {}
// Execute implements subcommands.Command.
func (*ptyRunner) Execute(_ context.Context, fs *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
c := exec.Command(fs.Args()[0], fs.Args()[1:]...)
f, err := pty.Start(c)
if err != nil {
fmt.Printf("pty.Start failed: %v", err)
return subcommands.ExitFailure
}
defer f.Close()
// Copy stdout from the command to keep this process alive until the
// subprocess exits.
io.Copy(os.Stdout, f)
return subcommands.ExitSuccess
}