Browse code

Windows: docker top implementation

Signed-off-by: John Howard <jhoward@microsoft.com>

John Howard authored on 2016/08/18 07:46:28
Showing 13 changed files
... ...
@@ -2,31 +2,52 @@ package daemon
2 2
 
3 3
 import (
4 4
 	"errors"
5
-	"strconv"
5
+	"fmt"
6
+	"time"
6 7
 
7 8
 	"github.com/docker/docker/api/types"
9
+	"github.com/docker/go-units"
8 10
 )
9 11
 
10
-// ContainerTop is a minimal implementation on Windows currently.
11
-// TODO Windows: This needs more work, but needs platform API support.
12
-// All we can currently return (particularly in the case of Hyper-V containers)
13
-// is a PID and the command.
14
-func (daemon *Daemon) ContainerTop(containerID string, psArgs string) (*types.ContainerProcessList, error) {
15
-
16
-	// It's really not an equivalent to linux 'ps' on Windows
12
+// ContainerTop handles `docker top` client requests.
13
+// Future considerations:
14
+// -- Windows users are far more familiar with CPU% total.
15
+//    Further, users on Windows rarely see user/kernel CPU stats split.
16
+//    The kernel returns everything in terms of 100ns. To obtain
17
+//    CPU%, we could do something like docker stats does which takes two
18
+//    samples, subtract the difference and do the maths. Unfortunately this
19
+//    would slow the stat call down and require two kernel calls. So instead,
20
+//    we do something similar to linux and display the CPU as combined HH:MM:SS.mmm.
21
+// -- Perhaps we could add an argument to display "raw" stats
22
+// -- "Memory" is an extremely overloaded term in Windows. Hence we do what
23
+//    task manager does and use the private working set as the memory counter.
24
+//    We could return more info for those who really understand how memory
25
+//    management works in Windows if we introduced a "raw" stats (above).
26
+func (daemon *Daemon) ContainerTop(name string, psArgs string) (*types.ContainerProcessList, error) {
27
+	// It's not at all an equivalent to linux 'ps' on Windows
17 28
 	if psArgs != "" {
18 29
 		return nil, errors.New("Windows does not support arguments to top")
19 30
 	}
20 31
 
21
-	s, err := daemon.containerd.Summary(containerID)
32
+	container, err := daemon.GetContainer(name)
22 33
 	if err != nil {
23 34
 		return nil, err
24 35
 	}
25 36
 
37
+	s, err := daemon.containerd.Summary(container.ID)
38
+	if err != nil {
39
+		return nil, err
40
+	}
26 41
 	procList := &types.ContainerProcessList{}
42
+	procList.Titles = []string{"Name", "PID", "CPU", "Private Working Set"}
27 43
 
28
-	for _, v := range s {
29
-		procList.Titles = append(procList.Titles, strconv.Itoa(int(v.Pid))+" "+v.Command)
44
+	for _, j := range s {
45
+		d := time.Duration((j.KernelTime100ns + j.UserTime100ns) * 100) // Combined time in nanoseconds
46
+		procList.Processes = append(procList.Processes, []string{
47
+			j.ImageName,
48
+			fmt.Sprint(j.ProcessId),
49
+			fmt.Sprintf("%02d:%02d:%02d.%03d", int(d.Hours()), int(d.Minutes())%60, int(d.Seconds())%60, int(d.Nanoseconds()/1000000)%1000),
50
+			units.HumanSize(float64(j.MemoryWorkingSetPrivateBytes))})
30 51
 	}
31 52
 	return procList, nil
32 53
 }
... ...
@@ -29,7 +29,7 @@ func (s *DockerSuite) BenchmarkConcurrentContainerActions(c *check.C) {
29 29
 				defer innerGroup.Done()
30 30
 				for i := 0; i < numIterations; i++ {
31 31
 					args := []string{"run", "-d", defaultSleepImage}
32
-					args = append(args, defaultSleepCommand...)
32
+					args = append(args, sleepCommandForDaemonPlatform()...)
33 33
 					out, _, err := dockerCmdWithError(args...)
34 34
 					if err != nil {
35 35
 						chErr <- fmt.Errorf(out)
... ...
@@ -881,7 +881,7 @@ func (s *DockerSuite) TestContainerApiStart(c *check.C) {
881 881
 	name := "testing-start"
882 882
 	config := map[string]interface{}{
883 883
 		"Image":     "busybox",
884
-		"Cmd":       append([]string{"/bin/sh", "-c"}, defaultSleepCommand...),
884
+		"Cmd":       append([]string{"/bin/sh", "-c"}, sleepCommandForDaemonPlatform()...),
885 885
 		"OpenStdin": true,
886 886
 	}
887 887
 
... ...
@@ -1117,7 +1117,7 @@ func (s *DockerSuite) TestContainerApiChunkedEncoding(c *check.C) {
1117 1117
 
1118 1118
 	config := map[string]interface{}{
1119 1119
 		"Image":     "busybox",
1120
-		"Cmd":       append([]string{"/bin/sh", "-c"}, defaultSleepCommand...),
1120
+		"Cmd":       append([]string{"/bin/sh", "-c"}, sleepCommandForDaemonPlatform()...),
1121 1121
 		"OpenStdin": true,
1122 1122
 	}
1123 1123
 	b, err := json.Marshal(config)
... ...
@@ -640,7 +640,7 @@ func (s *DockerSuite) TestPsImageIDAfterUpdate(c *check.C) {
640 640
 	originalImageID, err := getIDByName(originalImageName)
641 641
 	c.Assert(err, checker.IsNil)
642 642
 
643
-	runCmd = exec.Command(dockerBinary, append([]string{"run", "-d", originalImageName}, defaultSleepCommand...)...)
643
+	runCmd = exec.Command(dockerBinary, append([]string{"run", "-d", originalImageName}, sleepCommandForDaemonPlatform()...)...)
644 644
 	out, _, err = runCommandWithOutput(runCmd)
645 645
 	c.Assert(err, checker.IsNil)
646 646
 	containerID := strings.TrimSpace(out)
... ...
@@ -14,7 +14,7 @@ func (s *DockerSwarmSuite) TestServiceUpdatePort(c *check.C) {
14 14
 	d := s.AddDaemon(c, true, true)
15 15
 
16 16
 	serviceName := "TestServiceUpdatePort"
17
-	serviceArgs := append([]string{"service", "create", "--name", serviceName, "-p", "8080:8081", defaultSleepImage}, defaultSleepCommand...)
17
+	serviceArgs := append([]string{"service", "create", "--name", serviceName, "-p", "8080:8081", defaultSleepImage}, sleepCommandForDaemonPlatform()...)
18 18
 
19 19
 	// Create a service with a port mapping of 8080:8081.
20 20
 	out, err := d.Cmd(serviceArgs...)
... ...
@@ -4,32 +4,62 @@ import (
4 4
 	"strings"
5 5
 
6 6
 	"github.com/docker/docker/pkg/integration/checker"
7
+	icmd "github.com/docker/docker/pkg/integration/cmd"
7 8
 	"github.com/go-check/check"
8 9
 )
9 10
 
10 11
 func (s *DockerSuite) TestTopMultipleArgs(c *check.C) {
11
-	testRequires(c, DaemonIsLinux)
12
-	out, _ := dockerCmd(c, "run", "-i", "-d", "busybox", "top")
12
+	out, _ := runSleepingContainer(c, "-d")
13 13
 	cleanedContainerID := strings.TrimSpace(out)
14 14
 
15
-	out, _ = dockerCmd(c, "top", cleanedContainerID, "-o", "pid")
16
-	c.Assert(out, checker.Contains, "PID", check.Commentf("did not see PID after top -o pid: %s", out))
15
+	var expected icmd.Expected
16
+	switch daemonPlatform {
17
+	case "windows":
18
+		expected = icmd.Expected{ExitCode: 1, Err: "Windows does not support arguments to top"}
19
+	default:
20
+		expected = icmd.Expected{Out: "PID"}
21
+	}
22
+	result := dockerCmdWithResult("top", cleanedContainerID, "-o", "pid")
23
+	c.Assert(result, icmd.Matches, expected)
17 24
 }
18 25
 
19 26
 func (s *DockerSuite) TestTopNonPrivileged(c *check.C) {
20
-	testRequires(c, DaemonIsLinux)
21
-	out, _ := dockerCmd(c, "run", "-i", "-d", "busybox", "top")
27
+	out, _ := runSleepingContainer(c, "-d")
22 28
 	cleanedContainerID := strings.TrimSpace(out)
23 29
 
24 30
 	out1, _ := dockerCmd(c, "top", cleanedContainerID)
25 31
 	out2, _ := dockerCmd(c, "top", cleanedContainerID)
26 32
 	dockerCmd(c, "kill", cleanedContainerID)
27 33
 
28
-	c.Assert(out1, checker.Contains, "top", check.Commentf("top should've listed `top` in the process list, but failed the first time"))
29
-	c.Assert(out2, checker.Contains, "top", check.Commentf("top should've listed `top` in the process list, but failed the second time"))
34
+	// Windows will list the name of the launched executable which in this case is busybox.exe, without the parameters.
35
+	// Linux will display the command executed in the container
36
+	var lookingFor string
37
+	if daemonPlatform == "windows" {
38
+		lookingFor = "busybox.exe"
39
+	} else {
40
+		lookingFor = "top"
41
+	}
42
+
43
+	c.Assert(out1, checker.Contains, lookingFor, check.Commentf("top should've listed `%s` in the process list, but failed the first time", lookingFor))
44
+	c.Assert(out2, checker.Contains, lookingFor, check.Commentf("top should've listed `%s` in the process list, but failed the second time", lookingFor))
45
+}
46
+
47
+// TestTopWindowsCoreProcesses validates that there are lines for the critical
48
+// processes which are found in a Windows container. Note Windows is architecturally
49
+// very different to Linux in this regard.
50
+func (s *DockerSuite) TestTopWindowsCoreProcesses(c *check.C) {
51
+	testRequires(c, DaemonIsWindows)
52
+	out, _ := runSleepingContainer(c, "-d")
53
+	cleanedContainerID := strings.TrimSpace(out)
54
+	out1, _ := dockerCmd(c, "top", cleanedContainerID)
55
+	lookingFor := []string{"smss.exe", "csrss.exe", "wininit.exe", "services.exe", "lsass.exe", "CExecSvc.exe"}
56
+	for i, s := range lookingFor {
57
+		c.Assert(out1, checker.Contains, s, check.Commentf("top should've listed `%s` in the process list, but failed. Test case %d", s, i))
58
+	}
30 59
 }
31 60
 
32 61
 func (s *DockerSuite) TestTopPrivileged(c *check.C) {
62
+	// Windows does not support --privileged
33 63
 	testRequires(c, DaemonIsLinux, NotUserNamespace)
34 64
 	out, _ := dockerCmd(c, "run", "--privileged", "-i", "-d", "busybox", "top")
35 65
 	cleanedContainerID := strings.TrimSpace(out)
... ...
@@ -162,7 +162,7 @@ func (s *DockerSuite) TestDeprecatedPostContainersStartWithoutLinksInHostConfig(
162 162
 	// An alternate test could be written to validate the negative testing aspect of this
163 163
 	testRequires(c, DaemonIsLinux)
164 164
 	name := "test-host-config-links"
165
-	dockerCmd(c, append([]string{"create", "--name", name, "busybox"}, defaultSleepCommand...)...)
165
+	dockerCmd(c, append([]string{"create", "--name", name, "busybox"}, sleepCommandForDaemonPlatform()...)...)
166 166
 
167 167
 	hc := inspectFieldJSON(c, name, "HostConfig")
168 168
 	config := `{"HostConfig":` + hc + `}`
... ...
@@ -1435,7 +1435,7 @@ func runSleepingContainerInImage(c *check.C, image string, extraArgs ...string)
1435 1435
 	args := []string{"run", "-d"}
1436 1436
 	args = append(args, extraArgs...)
1437 1437
 	args = append(args, image)
1438
-	args = append(args, defaultSleepCommand...)
1438
+	args = append(args, sleepCommandForDaemonPlatform()...)
1439 1439
 	return dockerCmd(c, args...)
1440 1440
 }
1441 1441
 
1442 1442
new file mode 100644
... ...
@@ -0,0 +1,11 @@
0
+package main
1
+
2
+// sleepCommandForDaemonPlatform is a helper function that determines what
3
+// the command is for a sleeping container based on the daemon platform.
4
+// The Windows busybox image does not have a `top` command.
5
+func sleepCommandForDaemonPlatform() []string {
6
+	if daemonPlatform == "windows" {
7
+		return []string{"sleep", "240"}
8
+	}
9
+	return []string{"top"}
10
+}
... ...
@@ -12,5 +12,3 @@ const (
12 12
 	// runs indefinitely while still being interruptible by a signal.
13 13
 	defaultSleepImage = "busybox"
14 14
 )
15
-
16
-var defaultSleepCommand = []string{"top"}
... ...
@@ -13,6 +13,3 @@ const (
13 13
 	// on `sleep` with a high duration.
14 14
 	defaultSleepImage = "busybox"
15 15
 )
16
-
17
-// TODO Windows: In TP5, decrease this sleep time, as performance will be better
18
-var defaultSleepCommand = []string{"sleep", "240"}
... ...
@@ -410,26 +410,23 @@ func (clnt *client) GetPidsForContainer(containerID string) ([]int, error) {
410 410
 // visible on the container host. However, libcontainerd does have
411 411
 // that information.
412 412
 func (clnt *client) Summary(containerID string) ([]Summary, error) {
413
-	var s []Summary
413
+
414
+	// Get the libcontainerd container object
414 415
 	clnt.lock(containerID)
415 416
 	defer clnt.unlock(containerID)
416
-	cont, err := clnt.getContainer(containerID)
417
+	container, err := clnt.getContainer(containerID)
417 418
 	if err != nil {
418 419
 		return nil, err
419 420
 	}
420
-
421
-	// Add the first process
422
-	s = append(s, Summary{
423
-		Pid:     cont.containerCommon.systemPid,
424
-		Command: cont.ociSpec.Process.Args[0]})
425
-	// And add all the exec'd processes
426
-	for _, p := range cont.processes {
427
-		s = append(s, Summary{
428
-			Pid:     p.processCommon.systemPid,
429
-			Command: p.commandLine})
421
+	p, err := container.hcsContainer.ProcessList()
422
+	if err != nil {
423
+		return nil, err
430 424
 	}
431
-	return s, nil
432
-
425
+	pl := make([]Summary, len(p))
426
+	for i := range p {
427
+		pl[i] = Summary(p[i])
428
+	}
429
+	return pl, nil
433 430
 }
434 431
 
435 432
 // UpdateResources updates resources for a running container.
... ...
@@ -1,6 +1,9 @@
1 1
 package libcontainerd
2 2
 
3
-import "github.com/docker/docker/libcontainerd/windowsoci"
3
+import (
4
+	"github.com/Microsoft/hcsshim"
5
+	"github.com/docker/docker/libcontainerd/windowsoci"
6
+)
4 7
 
5 8
 // Spec is the base configuration for the container.
6 9
 type Spec windowsoci.WindowsSpec
... ...
@@ -11,11 +14,8 @@ type Process windowsoci.Process
11 11
 // User specifies user information for the containers main process.
12 12
 type User windowsoci.User
13 13
 
14
-// Summary contains a container summary from containerd
15
-type Summary struct {
16
-	Pid     uint32
17
-	Command string
18
-}
14
+// Summary contains a ProcessList item from HCS to support `top`
15
+type Summary hcsshim.ProcessListItem
19 16
 
20 17
 // StateInfo contains description about the new state container has entered.
21 18
 type StateInfo struct {