rmmagent/agent/system/system.go
2022-06-23 15:07:18 -07:00

267 lines
5.2 KiB
Go

package system
import (
"bytes"
"context"
"errors"
"fmt"
"io/ioutil"
"math"
"os"
"os/exec"
"strconv"
"time"
"github.com/amidaware/rmmagent/agent/tactical/shared"
"github.com/amidaware/rmmagent/agent/utils"
ps "github.com/elastic/go-sysinfo"
gocmd "github.com/go-cmd/cmd"
"github.com/shirou/gopsutil/cpu"
gops "github.com/shirou/gopsutil/v3/process"
)
type CmdStatus struct {
Status gocmd.Status
Stdout string
Stderr string
}
func NewCMDOpts() *CmdOptions {
return &CmdOptions{
Shell: "/bin/bash",
Timeout: 30,
}
}
func CmdV2(c *CmdOptions) CmdStatus {
ctx, cancel := context.WithTimeout(context.Background(), c.Timeout*time.Second)
defer cancel()
// Disable output buffering, enable streaming
cmdOptions := gocmd.Options{
Buffered: false,
Streaming: true,
}
// have a child process that is in a different process group so that
// parent terminating doesn't kill child
if c.Detached {
cmdOptions.BeforeExec = []func(cmd *exec.Cmd){
func(cmd *exec.Cmd) {
cmd.SysProcAttr = SetDetached()
},
}
}
var envCmd *gocmd.Cmd
if c.IsScript {
envCmd = gocmd.NewCmdOptions(cmdOptions, c.Shell, c.Args...) // call script directly
} else if c.IsExecutable {
envCmd = gocmd.NewCmdOptions(cmdOptions, c.Shell, c.Command) // c.Shell: bin + c.Command: args as one string
} else {
envCmd = gocmd.NewCmdOptions(cmdOptions, c.Shell, "-c", c.Command) // /bin/bash -c 'ls -l /var/log/...'
}
var stdoutBuf bytes.Buffer
var stderrBuf bytes.Buffer
// Print STDOUT and STDERR lines streaming from Cmd
doneChan := make(chan struct{})
go func() {
defer close(doneChan)
// Done when both channels have been closed
// https://dave.cheney.net/2013/04/30/curious-channels
for envCmd.Stdout != nil || envCmd.Stderr != nil {
select {
case line, open := <-envCmd.Stdout:
if !open {
envCmd.Stdout = nil
continue
}
fmt.Fprintln(&stdoutBuf, line)
case line, open := <-envCmd.Stderr:
if !open {
envCmd.Stderr = nil
continue
}
fmt.Fprintln(&stderrBuf, line)
}
}
}()
// Run and wait for Cmd to return, discard Status
envCmd.Start()
go func() {
select {
case <-doneChan:
return
case <-ctx.Done():
pid := envCmd.Status().PID
KillProc(int32(pid))
}
}()
// Wait for goroutine to print everything
<-doneChan
ret := CmdStatus{
Status: envCmd.Status(),
Stdout: utils.CleanString(stdoutBuf.String()),
Stderr: utils.CleanString(stderrBuf.String()),
}
return ret
}
func RunPythonCode(code string, timeout int, args []string) (string, error) {
content := []byte(code)
dir, err := ioutil.TempDir("", "tacticalpy")
if err != nil {
//a.Logger.Debugln(err)
return "", err
}
defer os.RemoveAll(dir)
tmpfn, _ := ioutil.TempFile(dir, "*.py")
if _, err := tmpfn.Write(content); err != nil {
//a.Logger.Debugln(err)
return "", err
}
if err := tmpfn.Close(); err != nil {
//a.Logger.Debugln(err)
return "", err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
var outb, errb bytes.Buffer
cmdArgs := []string{tmpfn.Name()}
if len(args) > 0 {
cmdArgs = append(cmdArgs, args...)
}
//a.Logger.Debugln(cmdArgs)
cmd := exec.CommandContext(ctx, shared.GetPythonBin(), cmdArgs...)
cmd.Stdout = &outb
cmd.Stderr = &errb
cmdErr := cmd.Run()
if ctx.Err() == context.DeadlineExceeded {
//a.Logger.Debugln("RunPythonCode:", ctx.Err())
return "", ctx.Err()
}
if cmdErr != nil {
//a.Logger.Debugln("RunPythonCode:", cmdErr)
return "", cmdErr
}
if errb.String() != "" {
//a.Logger.Debugln(errb.String())
return errb.String(), errors.New("RunPythonCode stderr")
}
return outb.String(), nil
}
func GetHostname() string {
host, _ := ps.Host()
info := host.Info()
return info.Hostname
}
// TotalRAM returns total RAM in GB
func TotalRAM() float64 {
host, err := ps.Host()
if err != nil {
return 8.0
}
mem, err := host.Memory()
if err != nil {
return 8.0
}
return math.Ceil(float64(mem.Total) / 1073741824.0)
}
// BootTime returns system boot time as a unix timestamp
func BootTime() int64 {
host, err := ps.Host()
if err != nil {
return 1000
}
info := host.Info()
return info.BootTime.Unix()
}
func GetCPULoadAvg() int {
fallback := false
pyCode := `
import psutil
try:
print(int(round(psutil.cpu_percent(interval=10))), end='')
except:
print("pyerror", end='')
`
pypercent, err := RunPythonCode(pyCode, 13, []string{})
if err != nil || pypercent == "pyerror" {
fallback = true
}
i, err := strconv.Atoi(pypercent)
if err != nil {
fallback = true
}
if fallback {
percent, err := cpu.Percent(10*time.Second, false)
if err != nil {
return 0
}
return int(math.Round(percent[0]))
}
return i
}
func GetProcsRPC() []ProcessMsg {
ret := make([]ProcessMsg, 0)
procs, _ := ps.Processes()
for i, process := range procs {
p, err := process.Info()
if err != nil {
continue
}
if p.PID == 0 {
continue
}
m, _ := process.Memory()
proc, gerr := gops.NewProcess(int32(p.PID))
if gerr != nil {
continue
}
cpu, _ := proc.CPUPercent()
user, _ := proc.Username()
ret = append(ret, ProcessMsg{
Name: p.Name,
Pid: p.PID,
MemBytes: m.Resident,
Username: user,
UID: i,
CPU: fmt.Sprintf("%.1f", cpu),
})
}
return ret
}