Files
Daniel Pfeifer 622c4ed390 **runsc ps: Add process group ID (PGID) to output**
Add the process group ID (PGID) field to the Process struct and
populate it using the existing kernel ProcessGroup API. The PGID is
now displayed in both table and JSON output of `runsc ps`.

The JSON output format is changed from a bare PID array to the full
Process struct serialization, making fields like PGID, PPID, UID, and
command name available to callers.

This enables container runtimes to discover process group IDs from
outside the sandbox without needing to exec into the container, which
is useful for sending signals to entire process groups via `runsc kill`
when the container may be under memory pressure.
2026-04-08 10:13:27 +00:00

169 lines
3.8 KiB
Go

// Copyright 2018 The gVisor 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 control
import (
"testing"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sentry/ktime"
"gvisor.dev/gvisor/pkg/sentry/usage"
)
func init() {
log.SetLevel(log.Debug)
}
// Tests that ProcessData.Table() prints with the correct format.
func TestProcessListTable(t *testing.T) {
testCases := []struct {
pl []*Process
expected string
}{
{
pl: []*Process{},
expected: "UID PID PPID PGID C TTY STIME TIME CMD",
},
{
pl: []*Process{
{
UID: 0,
PID: 0,
PPID: 0,
PGID: 0,
C: 0,
TTY: "?",
STime: "0",
Time: "0",
Cmd: "zero",
},
{
UID: 1,
PID: 1,
PPID: 1,
PGID: 1,
C: 1,
TTY: "pts/4",
STime: "1",
Time: "1",
Cmd: "one",
},
},
expected: `UID PID PPID PGID C TTY STIME TIME CMD
0 0 0 0 0 ? 0 0 zero
1 1 1 1 1 pts/4 1 1 one`,
},
}
for _, tc := range testCases {
output := ProcessListToTable(tc.pl)
if tc.expected != output {
t.Errorf("PrintTable(%v): got:\n%s\nwant:\n%s", tc.pl, output, tc.expected)
}
}
}
func TestProcessListJSON(t *testing.T) {
testCases := []struct {
pl []*Process
expected string
}{
{
pl: []*Process{},
expected: "[]",
},
{
pl: []*Process{
{
UID: 0,
PID: 0,
PPID: 0,
C: 0,
STime: "0",
Time: "0",
Cmd: "zero",
},
{
UID: 1,
PID: 1,
PPID: 1,
C: 1,
STime: "1",
Time: "1",
Cmd: "one",
},
},
expected: "[0,1]",
},
}
for _, tc := range testCases {
output, err := PrintPIDsJSON(tc.pl)
if err != nil {
t.Errorf("failed to generate JSON: %v", err)
}
if tc.expected != output {
t.Errorf("PrintJSON(%v): got:\n%s\nwant:\n%s", tc.pl, output, tc.expected)
}
}
}
func TestPercentCPU(t *testing.T) {
testCases := []struct {
stats usage.CPUStats
startTime ktime.Time
now ktime.Time
expected int32
}{
{
// Verify that 100% use is capped at 99.
stats: usage.CPUStats{UserTime: 1e9, SysTime: 1e9},
startTime: ktime.FromNanoseconds(7e9),
now: ktime.FromNanoseconds(9e9),
expected: 99,
},
{
// Verify that if usage > lifetime, we get at most 99%
// usage.
stats: usage.CPUStats{UserTime: 2e9, SysTime: 2e9},
startTime: ktime.FromNanoseconds(7e9),
now: ktime.FromNanoseconds(9e9),
expected: 99,
},
{
// Verify that 50% usage is reported correctly.
stats: usage.CPUStats{UserTime: 1e9, SysTime: 1e9},
startTime: ktime.FromNanoseconds(12e9),
now: ktime.FromNanoseconds(16e9),
expected: 50,
},
{
// Verify that 0% usage is reported correctly.
stats: usage.CPUStats{UserTime: 0, SysTime: 0},
startTime: ktime.FromNanoseconds(12e9),
now: ktime.FromNanoseconds(14e9),
expected: 0,
},
}
for _, tc := range testCases {
if pcpu := percentCPU(tc.stats, tc.startTime, tc.now); pcpu != tc.expected {
t.Errorf("percentCPU(%v, %v, %v): got %d, want %d", tc.stats, tc.startTime, tc.now, pcpu, tc.expected)
}
}
}