internal/ps/ps_linux.go (203 lines of code) (raw):
// Copyright 2024 Google LLC
//
// 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.
//go:build linux
package ps
import (
"context"
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"github.com/GoogleCloudPlatform/galog"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/run"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/file"
)
// linuxClient is for finding processes on linux distributions.
type linuxClient struct {
commonClient
// procDir is the location of proc filesystem mount point in a linux system.
procDir string
}
const (
// defaultLinuxProcDir is the default location of proc filesystem mount point
// in a linux system.
defaultLinuxProcDir = "/proc/"
)
// clkTime caches the [CLK_TCK] value for computing CPU usage.
var clkTime float64
// init creates the Linux process finder.
func init() {
Client = &linuxClient{
procDir: defaultLinuxProcDir,
}
}
// readClkTicks reads the [CLK_TCK] value by running [getconf CLK_TCK] command.
func readClkTicks(ctx context.Context) (float64, error) {
opts := run.Options{
Name: "getconf",
Args: []string{"CLK_TCK"},
OutputType: run.OutputStdout,
}
clkRes, err := run.WithContext(ctx, opts)
if err != nil {
return 0, fmt.Errorf("getconf CLK_TCK failed: %w", err)
}
return strconv.ParseFloat(strings.TrimSpace(string(clkRes.Output)), 64)
}
func (p linuxClient) FindPid(pid int) (Process, error) {
return p.readPidDetails(pid)
}
// FindRegex finds all processes with the executable path matching the provided
// regex.
func (p linuxClient) FindRegex(exeMatch string) ([]Process, error) {
var result []Process
procExpression, err := regexp.Compile("^[0-9]+$")
if err != nil {
return nil, fmt.Errorf("failed to compile process dir expression: %w", err)
}
exeExpression, err := regexp.Compile(exeMatch)
if err != nil {
return nil, fmt.Errorf("failed to compile process exec matching expression: %w", err)
}
files, err := os.ReadDir(p.procDir)
if err != nil {
return nil, fmt.Errorf("failed to read linux proc dir: %w", err)
}
for _, file := range files {
if !file.IsDir() {
continue
}
if !procExpression.MatchString(file.Name()) {
continue
}
// Ignore the error due to the `procExpression` regex, which ensures that
// the file name is a valid PID, and therefore we should not expect any
// errors.
pid, _ := strconv.Atoi(file.Name())
process, err := p.readPidDetails(pid)
if err != nil {
galog.Debugf("Failed to read process(%d), while finding processes matching regex %q: [%v], skipping...", pid, exeMatch, err)
continue
}
if !exeExpression.MatchString(process.Exe) {
continue
}
result = append(result, process)
}
return result, nil
}
func (p linuxClient) readPidDetails(pid int) (Process, error) {
var process Process
// Find the executable path.
processRootDir := filepath.Join(p.procDir, fmt.Sprintf("%d", pid))
if !file.Exists(processRootDir, file.TypeDir) {
return process, fmt.Errorf("process %d does not exist, %q not found", pid, processRootDir)
}
exeLinkPath := filepath.Join(processRootDir, "exe")
exePath, err := os.Readlink(exeLinkPath)
if err != nil {
return process, fmt.Errorf("error reading executable path: %w", err)
}
// Find the command line.
cmdlinePath := filepath.Join(processRootDir, "cmdline")
dat, err := os.ReadFile(cmdlinePath)
if err != nil {
return process, fmt.Errorf("error reading cmdline file: %w", err)
}
var commandLine []string
var token []byte
for _, curr := range dat {
if curr == 0 {
commandLine = append(commandLine, string(token))
token = nil
} else {
token = append(token, curr)
}
}
return Process{
PID: pid,
Exe: exePath,
CommandLine: commandLine,
}, nil
}
// Memory returns the memory usage in kB of the process with the provided PID.
func (p linuxClient) Memory(pid int) (int, error) {
baseProcDir := filepath.Join(p.procDir, strconv.Itoa(pid))
var stats []byte
var readErrors error
var readFile bool
var openFile string
// Read the smaps file. This is where the memory usage of the process is
// stored.
for _, fpath := range []string{"smaps", "smaps_rollup"} {
var err error
openFile = filepath.Join(baseProcDir, fpath)
stats, err = os.ReadFile(openFile) // NOLINT
if err != nil {
// If the error is not "not exist" means we failed to read it, in that
// case we don't fallback to other files.
if !os.IsNotExist(err) {
return 0, fmt.Errorf("error reading %s file: %w", fpath, err)
}
// If the error is "not exist", we fallback to other files and keep track
// of the errors.
if readErrors == nil {
readErrors = err
} else {
readErrors = fmt.Errorf("%w; %w", readErrors, err)
}
}
readFile = true
if err == nil {
break
}
}
if !readFile && readErrors != nil {
return 0, fmt.Errorf("error reading smaps/smaps_rollup file: %w", readErrors)
}
statsLines := strings.Split(string(stats), "\n")
foundRss := false
var memUsage int
// Now find the memory line. This line is the RSS line.
for _, line := range statsLines {
if strings.HasPrefix(line, "Rss") {
foundRss = true
partial, err := strconv.Atoi(strings.Fields(line)[1])
if err != nil {
return 0, fmt.Errorf("error parsing RSS line: %w", err)
}
memUsage += partial
}
}
if !foundRss {
return 0, fmt.Errorf("no RSS line found in %s file", openFile)
}
return memUsage, nil
}
// CPUUsage returns the % CPU usage of the process with the provided PID.
func (p linuxClient) CPUUsage(ctx context.Context, pid int) (float64, error) {
baseProcDir := filepath.Join(p.procDir, strconv.Itoa(pid))
// Read the stat file. This is where the usage data is kept.
stats, err := os.ReadFile(filepath.Join(baseProcDir, "stat"))
if err != nil {
return 0, fmt.Errorf("error reading stat file: %w", err)
}
statsLines := strings.Fields(string(stats))
// Read utime and stime values. The fields are not labelled, so get them
// as-is.
utime, err := strconv.ParseFloat(statsLines[13], 64)
if err != nil {
return 0, fmt.Errorf("error parsing utime: %w", err)
}
stime, err := strconv.ParseFloat(statsLines[14], 64)
if err != nil {
return 0, fmt.Errorf("error parsing stime: %w", err)
}
// Total time used is the sum of both values.
// Since the values are in clock ticks, divide by clock tick.
totalTimeTicks := utime + stime
if clkTime == 0 {
clkTime, err = readClkTicks(ctx)
if err != nil {
return 0, fmt.Errorf("error reading clk time: %w", err)
}
}
runTime := totalTimeTicks / clkTime
// Get the process start time and system uptime. These make up for the total
// elapsed time since the process started.
startTimeTicks, err := strconv.ParseFloat(statsLines[21], 64)
if err != nil {
return 0, fmt.Errorf("error parsing start time: %w", err)
}
startTime := startTimeTicks / clkTime
// uptime is the total running time of the system.
uptimeContents, err := os.ReadFile(filepath.Join(p.procDir, "uptime"))
if err != nil {
return 0, fmt.Errorf("error reading /proc/uptime file: %w", err)
}
uptime, err := strconv.ParseFloat(strings.Fields(string(uptimeContents))[0], 64)
if err != nil {
return 0, fmt.Errorf("error parsing /proc/uptime: %w", err)
}
// Time the process spent running.
elapsedTime := uptime - startTime
// Divide by elapsed time.
return runTime / elapsedTime, nil
}
// IsProcessAlive returns true if the process with the provided PID is alive.
// It does not return an error in any case and caller can simply rely on the
// returned boolean. Error is part of the signature to be compatible with the
// other platforms.
func (p linuxClient) IsProcessAlive(pid int) (bool, error) {
galog.Debugf("Checking if process %d is alive", pid)
// Its safe to ignore the error, as per docs find process never returns error
// on unix systems.
proc, _ := os.FindProcess(pid)
err := proc.Signal(syscall.Signal(0))
if err != nil {
galog.Debugf("Process with pid %d not running, signal 0 returned error: %v", pid, err)
return false, nil
}
return true, nil
}