-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
n-cli run [your shell command here]
- run shell commands and ga…
…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
Showing
11 changed files
with
292 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,6 +34,7 @@ func init() { | |
NewWhereCmd(), | ||
NewInitCmd(), | ||
NewVersionCmd(), | ||
NewRunCmd(), | ||
) | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
//go:build !windows | ||
|
||
package monitor | ||
|
||
func IsWindows() bool { | ||
return false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} |