diff --git a/README.md b/README.md index ddfa1cd..24c7e6f 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ In fact, if `systemctl` isn't found in the `PATH`, this library will panic. - [x] Get start time of a service (`ExecMainStartTimestamp`) as a `Time` type - [x] Get current memory in bytes (`MemoryCurrent`) an an int - [x] Get the PID of the main process (`MainPID`) as an int +- [x] Get the restart count of a unit (`NRestarts`) as an int ## Useful errors @@ -45,10 +46,6 @@ All functions return a predefined error type, and it is highly recommended these All calls into this library support go's `context` functionality. Therefore, blocking calls can time out according to the caller's needs, and the returned error should be checked to see if a timeout occurred (`ErrExecTimeout`). -## TODO - -- [ ] Set up `go test` testing -- [ ] Document each function using `godoc` standards ## Simple example diff --git a/helpers.go b/helpers.go index 3359332..3b16ff0 100644 --- a/helpers.go +++ b/helpers.go @@ -24,6 +24,15 @@ func GetStartTime(ctx context.Context, unit string, opts Options) (time.Time, er return time.Parse(dateFormat, value) } +// Get the number of times a process restarted (`systemctl show [unit] --property NRestarts`) as an int +func GetNumRestarts(ctx context.Context, unit string, opts Options) (int, error) { + value, err := Show(ctx, unit, properties.NRestarts, opts) + if err != nil { + return -1, err + } + return strconv.Atoi(value) +} + // Get current memory in bytes (`systemctl show [unit] --property MemoryCurrent`) an an int func GetMemoryUsage(ctx context.Context, unit string, opts Options) (int, error) { value, err := Show(ctx, unit, properties.MemoryCurrent, opts) diff --git a/helpers_test.go b/helpers_test.go index 6ff29fc..9225265 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -3,6 +3,7 @@ package systemctl import ( "context" "fmt" + "syscall" "testing" "time" ) @@ -80,6 +81,83 @@ func TestGetStartTime(t *testing.T) { } }) +} +func TestGetNumRestarts(t *testing.T) { + testCases := []struct { + unit string + err error + opts Options + runAsUser bool + }{ + // Run these tests only as a user + + //try nonexistant unit in user mode as user + {"nonexistant", ErrValueNotSet, Options{UserMode: false}, true}, + // try existing unit in user mode as user + {"syncthing", ErrValueNotSet, Options{UserMode: true}, true}, + // try existing unit in system mode as user + {"nginx", nil, Options{UserMode: false}, true}, + + // Run these tests only as a superuser + + // try nonexistant unit in system mode as system + {"nonexistant", ErrValueNotSet, Options{UserMode: false}, false}, + // try existing unit in system mode as system + {"nginx", ErrBusFailure, Options{UserMode: true}, false}, + // try existing unit in system mode as system + {"nginx", nil, Options{UserMode: false}, false}, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) { + t.Parallel() + if (userString == "root" || userString == "system") && tc.runAsUser { + t.Skip("skipping user test while running as superuser") + } else if (userString != "root" && userString != "system") && !tc.runAsUser { + t.Skip("skipping superuser test while running as user") + } + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + _, err := GetNumRestarts(ctx, tc.unit, tc.opts) + if err != tc.err { + t.Errorf("error is %v, but should have been %v", err, tc.err) + } + }) + } + // Prove restart count increases by one after a restart + t.Run(fmt.Sprintf("prove restart count increases by one after a restart"), func(t *testing.T) { + if userString != "root" && userString != "system" { + t.Skip("skipping superuser test while running as user") + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + restarts, err := GetNumRestarts(ctx, "nginx", Options{UserMode: false}) + if err != nil { + t.Errorf("issue getting number of restarts for nginx: %v", err) + } + pid, err := GetPID(ctx, "nginx", Options{UserMode: false}) + if err != nil { + t.Errorf("issue getting MainPID for nginx as %s: %v", userString, err) + } + syscall.Kill(pid, syscall.SIGKILL) + for { + running, err := IsActive(ctx, "nginx", Options{UserMode: false}) + if err != nil { + t.Errorf("error asserting nginx is up: %v", err) + break + } else if running { + break + } + } + secondRestarts, err := GetNumRestarts(ctx, "nginx", Options{UserMode: false}) + if err != nil { + t.Errorf("issue getting second reading on number of restarts for nginx: %v", err) + } + if restarts+1 != secondRestarts { + t.Errorf("Expected restart count to differ by one, but difference was: %d", secondRestarts-restarts) + } + }) + } func TestGetMemoryUsage(t *testing.T) { @@ -123,7 +201,7 @@ func TestGetMemoryUsage(t *testing.T) { } }) } - // Prove start time changes after a restart + // Prove memory usage values change across services t.Run(fmt.Sprintf("prove memory usage values change across services"), func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() @@ -133,12 +211,11 @@ func TestGetMemoryUsage(t *testing.T) { } secondBytes, err := GetMemoryUsage(ctx, "user.slice", Options{UserMode: false}) if err != nil { - t.Errorf("issue getting second memort usage reading of nginx: %v", err) + t.Errorf("issue getting second memory usage reading of nginx: %v", err) } if bytes == secondBytes { t.Errorf("Expected memory usage between nginx and user.slice to differ, but both were: %d", bytes) } - t.Parallel() }) } @@ -184,6 +261,3 @@ func TestGetPID(t *testing.T) { }) } } - -// GetMemoryUsage(ctx context.Context, unit string, opts Options) (int, error) { -// GetPID(ctx context.Context, unit string, opts Options) (int, error) { diff --git a/systemctl.go b/systemctl.go index d966c5f..e779904 100644 --- a/systemctl.go +++ b/systemctl.go @@ -62,15 +62,19 @@ func IsActive(ctx context.Context, unit string, opts Options) (bool, error) { args[1] = "--user" } stdout, _, _, err := execute(ctx, args) - if matched, _ := regexp.MatchString(`inactive`, stdout); matched { + stdout = strings.TrimSuffix(stdout, "\n") + switch stdout { + case "inactive": return false, nil - } else if matched, _ := regexp.MatchString(`active`, stdout); matched { + case "active": return true, nil - } else if matched, _ := regexp.MatchString(`failed`, stdout); matched { + case "failed": return false, nil + case "activating": + return false, nil + default: + return false, err } - - return false, err } // Checks whether any of the specified unit files are enabled (as with enable). @@ -88,6 +92,7 @@ func IsEnabled(ctx context.Context, unit string, opts Options) (bool, error) { args[1] = "--user" } stdout, _, _, err := execute(ctx, args) + stdout = strings.TrimSuffix(stdout, "\n") switch stdout { case "enabled": return true, nil