Signed-off-by: John Howard <jhoward@microsoft.com>
John Howard authored on 2016/08/18 07:46:28... | ... |
@@ -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 |
+} |
... | ... |
@@ -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 { |