Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the top command on Windows #2038

Merged
merged 1 commit into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux || darwin || freebsd || netbsd || openbsd
dardelean marked this conversation as resolved.
Show resolved Hide resolved

/*
Copyright The containerd Authors.

Expand Down
60 changes: 60 additions & 0 deletions cmd/nerdctl/container_top_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"testing"

"github.com/containerd/nerdctl/pkg/testutil"
"golang.org/x/sys/windows/svc/mgr"
)

func TestTopProcessContainer(t *testing.T) {
testContainerName := testutil.Identifier(t)

base := testutil.NewBase(t)
defer base.Cmd("rm", "-f", testContainerName).Run()

base.Cmd("run", "-d", "--name", testContainerName, testutil.WindowsNano, "sleep", "5").AssertOK()
base.Cmd("top", testContainerName).AssertOK()
}

func TestTopHyperVContainer(t *testing.T) {
// Hyper-V Virtual Machine Management service
hypervServiceName := "vmms"

m, err := mgr.Connect()
if err != nil {
t.Fatalf("unable to access Windows service control manager: %s", err)
}
defer m.Disconnect()

s, err := m.OpenService(hypervServiceName)
// hyperv service is not present, hyperv is not enabled
if err != nil {
t.Skip("HyperV is not enabled, skipping test")
}
defer s.Close()
Comment on lines +38 to +51
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm adding a little helper we can use for future hyperv tests here #2062


testContainerName := testutil.Identifier(t)

base := testutil.NewBase(t)
defer base.Cmd("rm", "-f", testContainerName).Run()

base.Cmd("run", "--isolation", "hyperv", "-d", "--name", testContainerName, testutil.WindowsNano, "sleep", "5").AssertOK()
base.Cmd("top", testContainerName).AssertOK()
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.19
require (
github.com/Masterminds/semver/v3 v3.2.0
github.com/Microsoft/go-winio v0.6.0
github.com/Microsoft/hcsshim v0.10.0-rc.7
github.com/compose-spec/compose-go v1.13.0
github.com/containerd/accelerated-container-image v0.6.0
github.com/containerd/cgroups v1.1.0
Expand Down Expand Up @@ -61,7 +62,6 @@ require (
require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20221215162035-5330a85ea652 // indirect
github.com/Microsoft/hcsshim v0.10.0-rc.7 // indirect
github.com/cilium/ebpf v0.9.1 // indirect
github.com/containerd/cgroups/v3 v3.0.1 // indirect
github.com/containerd/fifo v1.1.0 // indirect
Expand Down
85 changes: 0 additions & 85 deletions pkg/cmd/container/top.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,11 @@
package container

import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os/exec"
"regexp"
"strconv"
"strings"
"text/tabwriter"

"github.com/containerd/containerd"
"github.com/containerd/nerdctl/pkg/api/types"
Expand Down Expand Up @@ -203,83 +198,3 @@ func parsePSOutput(output []byte, procs []uint32) (*ContainerTopOKBody, error) {
}
return procList, nil
}

dardelean marked this conversation as resolved.
Show resolved Hide resolved
// containerTop was inspired from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L133-L189
//
// ContainerTop lists the processes running inside of the given
// container by calling ps with the given args, or with the flags
// "-ef" if no args are given. An error is returned if the container
// is not found, or is not running, or if there are any problems
// running ps, or parsing the output.
func containerTop(ctx context.Context, stdio io.Writer, client *containerd.Client, id string, psArgs string) error {
if psArgs == "" {
psArgs = "-ef"
}

if err := validatePSArgs(psArgs); err != nil {
return err
}

container, err := client.LoadContainer(ctx, id)
if err != nil {
return err
}

task, err := container.Task(ctx, nil)
if err != nil {
return err
}

status, err := task.Status(ctx)
if err != nil {
return err
}

if status.Status != containerd.Running {
return nil
}

//TO DO handle restarting case: wait for container to restart and then launch top command

procs, err := task.Pids(ctx)
if err != nil {
return err
}

psList := make([]uint32, 0, len(procs))
for _, ps := range procs {
psList = append(psList, ps.Pid)
}

args := strings.Split(psArgs, " ")
pids := psPidsArg(psList)
output, err := exec.Command("ps", append(args, pids)...).Output()
if err != nil {
// some ps options (such as f) can't be used together with q,
// so retry without it
output, err = exec.Command("ps", args...).Output()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
// first line of stderr shows why ps failed
line := bytes.SplitN(ee.Stderr, []byte{'\n'}, 2)
if len(line) > 0 && len(line[0]) > 0 {
return errors.New(string(line[0]))
}
}
return nil
}
}
procList, err := parsePSOutput(output, psList)
if err != nil {
return err
}

w := tabwriter.NewWriter(stdio, 20, 1, 3, ' ', 0)
fmt.Fprintln(w, strings.Join(procList.Titles, "\t"))

for _, proc := range procList.Processes {
fmt.Fprintln(w, strings.Join(proc, "\t"))
}

return w.Flush()
}
122 changes: 122 additions & 0 deletions pkg/cmd/container/top_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//go:build linux || darwin || freebsd || netbsd || openbsd

/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/*
Portions from:
- https://github.com/moby/moby/blob/v20.10.6/api/types/container/container_top.go
- https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go
Copyright (C) The Moby authors.
Licensed under the Apache License, Version 2.0
NOTICE: https://github.com/moby/moby/blob/v20.10.6/NOTICE
*/

package container

import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os/exec"
"strings"
"text/tabwriter"

"github.com/containerd/containerd"
)

// containerTop was inspired from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L133-L189
//
// ContainerTop lists the processes running inside of the given
// container by calling ps with the given args, or with the flags
// "-ef" if no args are given. An error is returned if the container
// is not found, or is not running, or if there are any problems
// running ps, or parsing the output.
// procList *ContainerTopOKBody
func containerTop(ctx context.Context, stdio io.Writer, client *containerd.Client, id string, psArgs string) error {
if psArgs == "" {
psArgs = "-ef"
}

if err := validatePSArgs(psArgs); err != nil {
return err
}

container, err := client.LoadContainer(ctx, id)
if err != nil {
return err
}

task, err := container.Task(ctx, nil)
if err != nil {
return err
}

status, err := task.Status(ctx)
if err != nil {
return err
}

if status.Status != containerd.Running {
return nil
}

//TO DO handle restarting case: wait for container to restart and then launch top command

procs, err := task.Pids(ctx)
if err != nil {
return err
}

psList := make([]uint32, 0, len(procs))
for _, ps := range procs {
psList = append(psList, ps.Pid)
}

args := strings.Split(psArgs, " ")
pids := psPidsArg(psList)
output, err := exec.Command("ps", append(args, pids)...).Output()
if err != nil {
// some ps options (such as f) can't be used together with q,
// so retry without it
output, err = exec.Command("ps", args...).Output()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
// first line of stderr shows why ps failed
line := bytes.SplitN(ee.Stderr, []byte{'\n'}, 2)
if len(line) > 0 && len(line[0]) > 0 {
return errors.New(string(line[0]))
}
}
return nil
}
}
procList, err := parsePSOutput(output, psList)
if err != nil {
return err
}

w := tabwriter.NewWriter(stdio, 20, 1, 3, ' ', 0)
fmt.Fprintln(w, strings.Join(procList.Titles, "\t"))

for _, proc := range procList.Processes {
fmt.Fprintln(w, strings.Join(proc, "\t"))
}

return w.Flush()
}
80 changes: 80 additions & 0 deletions pkg/cmd/container/top_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package container

import (
"context"
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"time"

"github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/options"
"github.com/containerd/containerd"
"github.com/containerd/typeurl/v2"
"github.com/docker/go-units"
)

// containerTop was inspired from https://github.com/moby/moby/blob/master/daemon/top_windows.go
//
// ContainerTop lists the processes running inside of the given
// container. An error is returned if the container
// is not found, or is not running.
func containerTop(ctx context.Context, stdio io.Writer, client *containerd.Client, id string, psArgs string) error {
container, err := client.LoadContainer(ctx, id)
if err != nil {
return err
}

task, err := container.Task(ctx, nil)
if err != nil {
return err
}
processes, err := task.Pids(ctx)
if err != nil {
return err
}
procList := &ContainerTopOKBody{}
procList.Titles = []string{"Name", "PID", "CPU", "Private Working Set"}

for _, j := range processes {
var info options.ProcessDetails
err = typeurl.UnmarshalTo(j.Info, &info)
if err != nil {
return err
}
d := time.Duration((info.KernelTime_100Ns + info.UserTime_100Ns) * 100) // Combined time in nanoseconds
procList.Processes = append(procList.Processes, []string{
info.ImageName,
fmt.Sprint(info.ProcessID),
fmt.Sprintf("%02d:%02d:%02d.%03d", int(d.Hours()), int(d.Minutes())%60, int(d.Seconds())%60, int(d.Nanoseconds()/1000000)%1000),
units.HumanSize(float64(info.MemoryWorkingSetPrivateBytes))})

}

w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0)
fmt.Fprintln(w, strings.Join(procList.Titles, "\t"))

for _, proc := range procList.Processes {
fmt.Fprintln(w, strings.Join(proc, "\t"))
}
w.Flush()

return nil
}