Skip to content

Commit

Permalink
Merge pull request #2038 from dardelean/feature/hyperv-containers
Browse files Browse the repository at this point in the history
Fix the top command on Windows
  • Loading branch information
AkihiroSuda committed Mar 21, 2023
2 parents ea92fda + ade009b commit 82a4d93
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux || darwin || freebsd || netbsd || openbsd

/*
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()

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
}

// 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
}

0 comments on commit 82a4d93

Please sign in to comment.