Signed-off-by: John Howard <jhoward@microsoft.com>
| ... | ... |
@@ -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 {
|