Skip to content

Commit

Permalink
Add n-cli run [your shell command here] - run shell commands and ga…
Browse files Browse the repository at this point in the history
…ther metrics that you can send to yourself as a notification (#2)

* add initial impl of pkg/notifier/marker; add n-cli run [your-command-here]

* marker: switch elapsedMs to elapsed.String()

* capture non-zero exit codes

* update description

* add CPU time monitor (get_cpu) to marker (TODO: check Windows compatibility)

* add memory usage tracker and number formatter

* monitor: add semi-support (at least so that it compiles) for Windows & unix 386

* fix build for linux 386

* add IsWindows() check to prevent fields that arent available on Windows from being sent in the message

* fix compilation err
  • Loading branch information
verzac authored Feb 1, 2024
1 parent d97c423 commit f557108
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 0 deletions.
41 changes: 41 additions & 0 deletions cmd/cmd_run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cmd

import (
"fmt"
"os"
"os/exec"

"github.com/lba-studio/n-cli/pkg/notifier/marker"
"github.com/spf13/cobra"
)

func NewRunCmd() *cobra.Command {
return &cobra.Command{
Use: "run",
Aliases: []string{"r"},
Args: cobra.MinimumNArgs(1),
Short: "Runs arbitrary shell commands through n-cli.",
Long: "Runs arbitrary shell commands. For example, `n-cli run echo Hello, world!` If you'd like to pass in flags to your shell command, use `n-cli run mycommand -- --flag1=true --flag2`.",
Run: func(cobraCmd *cobra.Command, args []string) {
command := args[0]
commandArgs := args[1:]

cmd := exec.Command(command, commandArgs...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

defer func() {
os.Exit(cmd.ProcessState.ExitCode())
}()

m := marker.NewNotificationMarker(cmd)
defer m.Done()

err := cmd.Run()
if err != nil {
fmt.Printf("n-cli run error: %s\n", err.Error())
}
},
}
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func init() {
NewWhereCmd(),
NewInitCmd(),
NewVersionCmd(),
NewRunCmd(),
)
}

Expand Down
40 changes: 40 additions & 0 deletions pkg/formatter/number.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package formatter

import (
"strconv"
)

func PrettyPrintInt64(num int64) string {
isNegative := false
if num < 0 {
isNegative = true
num = -num
}
numberTokens := make([]string, 0)
for {
numStr := strconv.FormatInt(num%1000, 10)
if num < 1000 {
numberTokens = append(numberTokens, numStr)
break
}
for len(numStr) < 3 {
numStr = "0" + numStr
}
numberTokens = append(numberTokens, numStr)
num = num / 1000
}
// convert to string
out := ""
for _, token := range numberTokens {
if out == "" {
out = token
} else {
out = token + "," + out
}
}
if isNegative {
return "-" + out
} else {
return out
}
}
30 changes: 30 additions & 0 deletions pkg/formatter/number_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package formatter

import (
"fmt"
"testing"
)

func TestPrettyPrintInt64(t *testing.T) {
testCases := []struct {
input int64
expected string
}{
{input: 0, expected: "0"},
{input: 123, expected: "123"},
{input: 1000, expected: "1,000"},
{input: 123456789, expected: "123,456,789"},
{input: 9876543210, expected: "9,876,543,210"},
{input: -123456789, expected: "-123,456,789"},
{input: -9876543210, expected: "-9,876,543,210"},
}

for _, tc := range testCases {
t.Run(fmt.Sprintf("input=%d expected=%s", tc.input, tc.expected), func(t *testing.T) {
result := PrettyPrintInt64(tc.input)
if result != tc.expected {
t.Errorf("Expected: %s, but got: %s", tc.expected, result)
}
})
}
}
8 changes: 8 additions & 0 deletions pkg/monitor/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package monitor

import "errors"

var (
ErrCallNotSupported = errors.New("this syscall / operation is unsupported")
ErrIsWindows = errors.New("not supported on Windows-based machines")
)
7 changes: 7 additions & 0 deletions pkg/monitor/is_windows_default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build !windows

package monitor

func IsWindows() bool {
return false
}
6 changes: 6 additions & 0 deletions pkg/monitor/is_windows_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package monitor

// IsWindows returns a magical
func IsWindows() bool {
return true
}
24 changes: 24 additions & 0 deletions pkg/monitor/unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//go:build !(linux && 386) && !windows

package monitor

import (
"os/exec"
"syscall"
)

// GetCPU returns the CPU time in nanoseconds
func GetCPU() (int64, error) {
usage := new(syscall.Rusage)
syscall.Getrusage(syscall.RUSAGE_CHILDREN, usage)
out := usage.Utime.Nano() + usage.Stime.Nano()
return out, nil
}

func GetMemoryFromCmd(cmd *exec.Cmd) (int64, error) {
rusage, ok := cmd.ProcessState.SysUsage().(*syscall.Rusage)
if !ok {
return 0, ErrCallNotSupported
}
return rusage.Maxrss, nil
}
24 changes: 24 additions & 0 deletions pkg/monitor/unix_386.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//go:build linux && 386

package monitor

import (
"os/exec"
"syscall"
)

// GetCPU returns the CPU time in nanoseconds
func GetCPU() (int64, error) {
usage := new(syscall.Rusage)
syscall.Getrusage(syscall.RUSAGE_CHILDREN, usage)
out := usage.Utime.Nano() + usage.Stime.Nano()
return int64(out), nil
}

func GetMemoryFromCmd(cmd *exec.Cmd) (int64, error) {
rusage, ok := cmd.ProcessState.SysUsage().(*syscall.Rusage)
if !ok {
return 0, ErrCallNotSupported
}
return int64(rusage.Maxrss), nil
}
18 changes: 18 additions & 0 deletions pkg/monitor/windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//go:build windows

package monitor

import (
"os/exec"
)

// GetCPU returns the CPU time in nanoseconds
func GetCPU() (int64, error) {
// not supported (for now?)
return 0, ErrIsWindows
}

func GetMemoryFromCmd(cmd *exec.Cmd) (int64, error) {
// not supported (for now?)
return 0, ErrIsWindows
}
93 changes: 93 additions & 0 deletions pkg/notifier/marker/marker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package marker

import (
"fmt"
"os/exec"
"strings"
"time"

"github.com/lba-studio/n-cli/pkg/formatter"
"github.com/lba-studio/n-cli/pkg/monitor"
"github.com/lba-studio/n-cli/pkg/notifier"
)

type NotificationMarker interface {
Done()
}

type NotificationMarkerImpl struct {
StartedFrom time.Time
Command *exec.Cmd
}

func NewNotificationMarker(cmd *exec.Cmd) NotificationMarker {
return &NotificationMarkerImpl{
StartedFrom: time.Now(),
Command: cmd,
}
}

type printedMarkerInfo struct {
exitCode int
elapsed string
cpuTime string
memoryUsage int64
}

func (m *NotificationMarkerImpl) formatMessage(info printedMarkerInfo) string {
status := "COMPLETE"
if info.exitCode > 0 {
status = "FAILED"
}
prettyCommand := strings.Join(m.Command.Args, " ")

infoStrings := []string{
fmt.Sprintf("Command `%s` %s.", prettyCommand, status),
fmt.Sprintf("Elapsed: %s", info.elapsed),
}
if !monitor.IsWindows() {
infoStrings = append(infoStrings,
fmt.Sprintf("CPU Time: %s", info.cpuTime),
fmt.Sprintf("Memory Usage: %s", formatter.PrettyPrintInt64(info.memoryUsage)),
)
}

return strings.Join(infoStrings, "\n")
}

func (m *NotificationMarkerImpl) Done() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Panic encountered while processing process information. Skipping analytics.", r)
}
}()
elapsed := time.Since(m.StartedFrom)
cpuTimeNano, err := monitor.GetCPU()
if err != nil && err != monitor.ErrIsWindows {
fmt.Printf("Cannot get cpuTimeNano: %s\n", err.Error())
return
}
cpuTime := time.Duration(time.Duration(cpuTimeNano) * time.Nanosecond)

memoryUsage, err := monitor.GetMemoryFromCmd(m.Command)
if err != nil && err != monitor.ErrIsWindows {
fmt.Printf("Cannot get memoryUsage: %s\n", err.Error())
return
}

exitCode := m.Command.ProcessState.ExitCode()
if exitCode < 0 {
return
}
msg := m.formatMessage(printedMarkerInfo{
memoryUsage: memoryUsage,
cpuTime: cpuTime.String(),
elapsed: elapsed.String(),
exitCode: exitCode,
})

err = notifier.Notify(msg)
if err != nil {
fmt.Printf("Error encountered when sending notification: %s\n", err.Error())
}
}

0 comments on commit f557108

Please sign in to comment.