15 Commits

Author SHA1 Message Date
4575e2016c fix(helpers): return ErrValueNotSet for nonexistent units in GetNumRestarts
systemd returns NRestarts=0 for units with LoadState=not-found, making
it indistinguishable from a genuinely loaded unit with zero restarts.
GetNumRestarts now checks LoadState when NRestarts is 0 and returns
ErrValueNotSet for units that don't exist, matching GetMemoryUsage
behavior.

Also adds unit tests for filterErr (all stderr error mapping cases)
and HasValidUnitSuffix (all valid unit types + negative cases).

Updates syncthing test expectation from ErrValueNotSet to nil since
loaded-but-inactive units legitimately have NRestarts=0.
2026-03-05 10:35:01 +00:00
22132919e5 feat: add optional variadic args to all commands (#8)
Allow callers to pass additional systemctl flags (e.g. --no-block,
--force) via variadic string args on every exported function.

This is backward-compatible: existing callers without extra args
continue to work unchanged.

Introduces a prepareArgs helper to centralize argument construction,
replacing the duplicated args/UserMode pattern across all functions.

Closes #2
2026-02-26 11:03:53 -05:00
d38136c0dc refactor: clean up unused code, fix typos, improve docs (#7)
* refactor: clean up unused code, fix typos, improve docs

- Remove unused 'killed' const and 'unitTypes' var (staticcheck U1000)
- Replace regexp with strings.TrimSuffix+switch in isFailed for consistency
- Fix typo: 'programatically' -> 'programmatically'
- Fix typo: 'an an int' -> 'as an int' in README and helpers.go
- Add missing godoc comments on exported helper functions
- Bump minimum Go version from 1.18 to 1.21

* refactor: use unused constants instead of removing them

- Export unitTypes as UnitTypes and add HasValidUnitSuffix helper
- Use killed const (exit code 130) in execute() to detect SIGINT
- Update go.mod to go 1.26
2026-02-23 00:01:59 -05:00
Dat Boi Diego
14c9f0f70d chore: add "support" for non-linux platforms (#6) 2025-09-20 19:34:22 -04:00
5f1537f8bc Merge pull request #4 from MatthiasKunnen/build-lines
Fix build lines conflict with go toolchain version
2025-05-23 14:48:39 -07:00
Matthias Kunnen
d38d347cc6 Fix build lines conflict with go toolchain version
21fce7918e adds build tags, however,
these specific tags are not supported in the go version set in go.mod (1.12).
See <https://go.dev/doc/go1.17#build-lines> and <https://go.dev/doc/go1.18#go-build-lines>.
2025-05-23 17:29:00 +02:00
14a2ca2acd add placehomer unexported unittypes for later 2025-02-18 00:34:16 -08:00
451a949ace add new properties, socket finder 2025-02-18 00:30:12 -08:00
21fce7918e add linux build tags to restrict compilation to linux targets 2025-02-13 16:49:49 -08:00
54f4f7a235 add IsSystemd checker 2025-02-12 19:21:48 -08:00
fa15432121 add ability to list all units 2024-08-08 15:37:04 -07:00
7bd5bef0cb fix broken error filtration 2023-06-28 23:43:20 -07:00
a82f845b84 add IsRunning helper 2023-06-20 23:49:23 -04:00
c9e7f79f8c add IsMasked utility 2023-06-20 23:40:22 -04:00
0075dc6b4d update to fix some tests, remove panics, and wrap errors 2023-06-17 23:18:28 -04:00
16 changed files with 1316 additions and 615 deletions

View File

@@ -5,7 +5,6 @@ This library aims at providing idiomatic `systemctl` bindings for go developers,
This tool tries to take guesswork out of arbitrarily shelling out to `systemctl` by providing a structured, thoroughly-tested wrapper for the `systemctl` functions most-likely to be used in a system program. This tool tries to take guesswork out of arbitrarily shelling out to `systemctl` by providing a structured, thoroughly-tested wrapper for the `systemctl` functions most-likely to be used in a system program.
If your system isn't running (or targeting another system running) `systemctl`, this library will be of little use to you. If your system isn't running (or targeting another system running) `systemctl`, this library will be of little use to you.
In fact, if `systemctl` isn't found in the `PATH`, this library will panic.
## What is systemctl ## What is systemctl
@@ -33,7 +32,7 @@ In fact, if `systemctl` isn't found in the `PATH`, this library will panic.
## Helper functionality ## Helper functionality
- [x] Get start time of a service (`ExecMainStartTimestamp`) as a `Time` type - [x] Get start time of a service (`ExecMainStartTimestamp`) as a `Time` type
- [x] Get current memory in bytes (`MemoryCurrent`) an an int - [x] Get current memory in bytes (`MemoryCurrent`) as an int
- [x] Get the PID of the main process (`MainPID`) as 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 - [x] Get the restart count of a unit (`NRestarts`) as an int

View File

@@ -21,7 +21,6 @@ var (
// Masked units can only be unmasked, but something else was attempted // Masked units can only be unmasked, but something else was attempted
// Unmask the unit before enabling or disabling it // Unmask the unit before enabling or disabling it
ErrMasked = errors.New("unit masked") ErrMasked = errors.New("unit masked")
// If this error occurs, the library isn't entirely useful, as it causes a panic
// Make sure systemctl is in the PATH before calling again // Make sure systemctl is in the PATH before calling again
ErrNotInstalled = errors.New("systemctl not in $PATH") ErrNotInstalled = errors.New("systemctl not in $PATH")
// A unit was expected to be running but was found inactive // A unit was expected to be running but was found inactive

View File

@@ -2,6 +2,7 @@ package systemctl
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"reflect" "reflect"
"runtime" "runtime"
@@ -12,10 +13,10 @@ import (
func TestErrorFuncs(t *testing.T) { func TestErrorFuncs(t *testing.T) {
errFuncs := []func(ctx context.Context, unit string, opts Options) error{ errFuncs := []func(ctx context.Context, unit string, opts Options) error{
Enable, func(ctx context.Context, unit string, opts Options) error { return Enable(ctx, unit, opts) },
Disable, func(ctx context.Context, unit string, opts Options) error { return Disable(ctx, unit, opts) },
Restart, func(ctx context.Context, unit string, opts Options) error { return Restart(ctx, unit, opts) },
Start, func(ctx context.Context, unit string, opts Options) error { return Start(ctx, unit, opts) },
} }
errCases := []struct { errCases := []struct {
unit string unit string
@@ -63,7 +64,7 @@ func TestErrorFuncs(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
err := f(ctx, tc.unit, tc.opts) err := f(ctx, tc.unit, tc.opts)
if err != tc.err { if !errors.Is(err, tc.err) {
t.Errorf("error is %v, but should have been %v", err, tc.err) t.Errorf("error is %v, but should have been %v", err, tc.err)
} }
}) })

119
filtererr_test.go Normal file
View File

@@ -0,0 +1,119 @@
package systemctl
import (
"errors"
"testing"
)
func TestFilterErr(t *testing.T) {
tests := []struct {
name string
stderr string
want error
}{
{
name: "empty stderr",
stderr: "",
want: nil,
},
{
name: "unit does not exist",
stderr: "Unit foo.service does not exist, proceeding anyway.",
want: ErrDoesNotExist,
},
{
name: "unit not found",
stderr: "Unit foo.service not found.",
want: ErrDoesNotExist,
},
{
name: "unit not loaded",
stderr: "Unit foo.service not loaded.",
want: ErrUnitNotLoaded,
},
{
name: "no such file or directory",
stderr: "No such file or directory",
want: ErrDoesNotExist,
},
{
name: "interactive authentication required",
stderr: "Interactive authentication required.",
want: ErrInsufficientPermissions,
},
{
name: "access denied",
stderr: "Access denied",
want: ErrInsufficientPermissions,
},
{
name: "dbus session bus address",
stderr: "Failed to connect to bus: $DBUS_SESSION_BUS_ADDRESS not set",
want: ErrBusFailure,
},
{
name: "unit is masked",
stderr: "Unit foo.service is masked.",
want: ErrMasked,
},
{
name: "generic failed",
stderr: "Failed to do something unknown",
want: ErrUnspecified,
},
{
name: "unrecognized warning",
stderr: "Warning: something benign happened",
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := filterErr(tt.stderr)
if tt.want == nil {
if got != nil {
t.Errorf("filterErr(%q) = %v, want nil", tt.stderr, got)
}
return
}
if !errors.Is(got, tt.want) {
t.Errorf("filterErr(%q) = %v, want error wrapping %v", tt.stderr, got, tt.want)
}
})
}
}
func TestHasValidUnitSuffix(t *testing.T) {
tests := []struct {
unit string
want bool
}{
{"nginx.service", true},
{"sshd.socket", true},
{"backup.timer", true},
{"dev-sda1.device", true},
{"home.mount", true},
{"dev-sda1.swap", true},
{"user.slice", true},
{"multi-user.target", true},
{"session-1.scope", true},
{"foo.automount", true},
{"backup.path", true},
{"foo.snapshot", true},
{"nginx", false},
{"", false},
{"foo.bar", false},
{"foo.services", false},
{".service", true},
}
for _, tt := range tests {
t.Run(tt.unit, func(t *testing.T) {
got := HasValidUnitSuffix(tt.unit)
if got != tt.want {
t.Errorf("HasValidUnitSuffix(%q) = %v, want %v", tt.unit, got, tt.want)
}
})
}
}

2
go.mod
View File

@@ -1,3 +1,3 @@
module github.com/taigrr/systemctl module github.com/taigrr/systemctl
go 1.17 go 1.26

View File

@@ -2,7 +2,10 @@ package systemctl
import ( import (
"context" "context"
"errors"
"os"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/taigrr/systemctl/properties" "github.com/taigrr/systemctl/properties"
@@ -29,10 +32,26 @@ func GetNumRestarts(ctx context.Context, unit string, opts Options) (int, error)
if err != nil { if err != nil {
return -1, err return -1, err
} }
return strconv.Atoi(value) if value == "[not set]" {
return -1, ErrValueNotSet
}
restarts, err := strconv.Atoi(value)
if err != nil {
return -1, err
}
// systemd returns NRestarts=0 for both genuinely zero-restart units and
// nonexistent/unloaded units. Disambiguate by checking LoadState: if the
// unit isn't loaded, the value is meaningless.
if restarts == 0 {
loadState, loadErr := Show(ctx, unit, properties.LoadState, opts)
if loadErr == nil && loadState == "not-found" {
return -1, ErrValueNotSet
}
}
return restarts, nil
} }
// Get current memory in bytes (`systemctl show [unit] --property MemoryCurrent`) an an int // Get current memory in bytes (`systemctl show [unit] --property MemoryCurrent`) as an int
func GetMemoryUsage(ctx context.Context, unit string, opts Options) (int, error) { func GetMemoryUsage(ctx context.Context, unit string, opts Options) (int, error) {
value, err := Show(ctx, unit, properties.MemoryCurrent, opts) value, err := Show(ctx, unit, properties.MemoryCurrent, opts)
if err != nil { if err != nil {
@@ -52,3 +71,118 @@ func GetPID(ctx context.Context, unit string, opts Options) (int, error) {
} }
return strconv.Atoi(value) return strconv.Atoi(value)
} }
// GetSocketsForServiceUnit returns the socket units associated with a given service unit.
func GetSocketsForServiceUnit(ctx context.Context, unit string, opts Options) ([]string, error) {
args := []string{"list-sockets", "--all", "--no-legend", "--no-pager"}
if opts.UserMode {
args = append(args, "--user")
}
stdout, _, _, err := execute(ctx, args)
if err != nil {
return []string{}, err
}
lines := strings.Split(stdout, "\n")
sockets := []string{}
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) < 3 {
continue
}
socketUnit := fields[1]
serviceUnit := fields[2]
if serviceUnit == unit+".service" {
sockets = append(sockets, socketUnit)
}
}
return sockets, nil
}
// GetUnits returns a list of all loaded units and their states.
func GetUnits(ctx context.Context, opts Options) ([]Unit, error) {
args := []string{"list-units", "--all", "--no-legend", "--full", "--no-pager"}
if opts.UserMode {
args = append(args, "--user")
}
stdout, stderr, _, err := execute(ctx, args)
if err != nil {
return []Unit{}, errors.Join(err, filterErr(stderr))
}
lines := strings.Split(stdout, "\n")
units := []Unit{}
for _, line := range lines {
entry := strings.Fields(line)
if len(entry) < 4 {
continue
}
unit := Unit{
Name: entry[0],
Load: entry[1],
Active: entry[2],
Sub: entry[3],
Description: strings.Join(entry[4:], " "),
}
units = append(units, unit)
}
return units, nil
}
// GetMaskedUnits returns a list of all masked unit names.
func GetMaskedUnits(ctx context.Context, opts Options) ([]string, error) {
args := []string{"list-unit-files", "--state=masked"}
if opts.UserMode {
args = append(args, "--user")
}
stdout, stderr, _, err := execute(ctx, args)
if err != nil {
return []string{}, errors.Join(err, filterErr(stderr))
}
lines := strings.Split(stdout, "\n")
units := []string{}
for _, line := range lines {
if !strings.Contains(line, "masked") {
continue
}
entry := strings.Split(line, " ")
if len(entry) < 3 {
continue
}
if entry[1] == "masked" {
unit := entry[0]
uName := strings.Split(unit, ".")
unit = uName[0]
units = append(units, unit)
}
}
return units, nil
}
// IsSystemd checks if systemd is the current init system by reading /proc/1/comm.
func IsSystemd() (bool, error) {
b, err := os.ReadFile("/proc/1/comm")
if err != nil {
return false, err
}
return strings.TrimSpace(string(b)) == "systemd", nil
}
// IsMasked checks if a unit is masked.
func IsMasked(ctx context.Context, unit string, opts Options) (bool, error) {
units, err := GetMaskedUnits(ctx, opts)
if err != nil {
return false, err
}
for _, u := range units {
if u == unit {
return true, nil
}
}
return false, nil
}
// IsRunning checks if a unit's sub-state is "running".
// See https://unix.stackexchange.com/a/396633 for details.
func IsRunning(ctx context.Context, unit string, opts Options) (bool, error) {
status, err := Show(ctx, unit, properties.SubState, opts)
return status == "running", err
}

View File

@@ -2,6 +2,7 @@ package systemctl
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"syscall" "syscall"
"testing" "testing"
@@ -58,13 +59,13 @@ func TestGetStartTime(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
_, err := GetStartTime(ctx, tc.unit, tc.opts) _, err := GetStartTime(ctx, tc.unit, tc.opts)
if err != tc.err { if !errors.Is(err, tc.err) {
t.Errorf("error is %v, but should have been %v", err, tc.err) t.Errorf("error is %v, but should have been %v", err, tc.err)
} }
}) })
} }
// Prove start time changes after a restart // Prove start time changes after a restart
t.Run(fmt.Sprintf("prove start time changes"), func(t *testing.T) { t.Run("prove start time changes", func(t *testing.T) {
if userString != "root" && userString != "system" { if userString != "root" && userString != "system" {
t.Skip("skipping superuser test while running as user") t.Skip("skipping superuser test while running as user")
} }
@@ -93,18 +94,19 @@ func TestGetStartTime(t *testing.T) {
} }
func TestGetNumRestarts(t *testing.T) { func TestGetNumRestarts(t *testing.T) {
testCases := []struct { type testCase struct {
unit string unit string
err error err error
opts Options opts Options
runAsUser bool runAsUser bool
}{ }
testCases := []testCase{
// Run these tests only as a user // Run these tests only as a user
// try nonexistant unit in user mode as user // try nonexistant unit in user mode as user
{"nonexistant", ErrValueNotSet, Options{UserMode: false}, true}, {"nonexistant", ErrValueNotSet, Options{UserMode: false}, true},
// try existing unit in user mode as user // try existing unit in user mode as user (loaded, so NRestarts=0 is valid)
{"syncthing", ErrValueNotSet, Options{UserMode: true}, true}, {"syncthing", nil, Options{UserMode: true}, true},
// try existing unit in system mode as user // try existing unit in system mode as user
{"nginx", nil, Options{UserMode: false}, true}, {"nginx", nil, Options{UserMode: false}, true},
@@ -118,23 +120,25 @@ func TestGetNumRestarts(t *testing.T) {
{"nginx", nil, Options{UserMode: false}, false}, {"nginx", nil, Options{UserMode: false}, false},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) { func(tc testCase) {
t.Parallel() t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
if (userString == "root" || userString == "system") && tc.runAsUser { t.Parallel()
t.Skip("skipping user test while running as superuser") if (userString == "root" || userString == "system") && tc.runAsUser {
} else if (userString != "root" && userString != "system") && !tc.runAsUser { t.Skip("skipping user test while running as superuser")
t.Skip("skipping superuser test while running as user") } 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() ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
_, err := GetNumRestarts(ctx, tc.unit, tc.opts) defer cancel()
if err != tc.err { _, err := GetNumRestarts(ctx, tc.unit, tc.opts)
t.Errorf("error is %v, but should have been %v", err, tc.err) if !errors.Is(err, tc.err) {
} t.Errorf("error is %v, but should have been %v", err, tc.err)
}) }
})
}(tc)
} }
// Prove restart count increases by one after a restart // 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) { t.Run("prove restart count increases by one after a restart", func(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping in short mode") t.Skip("skipping in short mode")
} }
@@ -154,9 +158,9 @@ func TestGetNumRestarts(t *testing.T) {
} }
syscall.Kill(pid, syscall.SIGKILL) syscall.Kill(pid, syscall.SIGKILL)
for { for {
running, err := IsActive(ctx, "nginx", Options{UserMode: false}) running, errIsActive := IsActive(ctx, "nginx", Options{UserMode: false})
if err != nil { if errIsActive != nil {
t.Errorf("error asserting nginx is up: %v", err) t.Errorf("error asserting nginx is up: %v", errIsActive)
break break
} else if running { } else if running {
break break
@@ -173,12 +177,13 @@ func TestGetNumRestarts(t *testing.T) {
} }
func TestGetMemoryUsage(t *testing.T) { func TestGetMemoryUsage(t *testing.T) {
testCases := []struct { type testCase struct {
unit string unit string
err error err error
opts Options opts Options
runAsUser bool runAsUser bool
}{ }
testCases := []testCase{
// Run these tests only as a user // Run these tests only as a user
// try nonexistant unit in user mode as user // try nonexistant unit in user mode as user
@@ -198,23 +203,25 @@ func TestGetMemoryUsage(t *testing.T) {
{"nginx", nil, Options{UserMode: false}, false}, {"nginx", nil, Options{UserMode: false}, false},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) { func(tc testCase) {
t.Parallel() t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
if (userString == "root" || userString == "system") && tc.runAsUser { t.Parallel()
t.Skip("skipping user test while running as superuser") if (userString == "root" || userString == "system") && tc.runAsUser {
} else if (userString != "root" && userString != "system") && !tc.runAsUser { t.Skip("skipping user test while running as superuser")
t.Skip("skipping superuser test while running as user") } 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() ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
_, err := GetMemoryUsage(ctx, tc.unit, tc.opts) defer cancel()
if err != tc.err { _, err := GetMemoryUsage(ctx, tc.unit, tc.opts)
t.Errorf("error is %v, but should have been %v", err, tc.err) if !errors.Is(err, tc.err) {
} t.Errorf("error is %v, but should have been %v", err, tc.err)
}) }
})
}(tc)
} }
// Prove memory usage values change across services // Prove memory usage values change across services
t.Run(fmt.Sprintf("prove memory usage values change across services"), func(t *testing.T) { t.Run("prove memory usage values change across services", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
bytes, err := GetMemoryUsage(ctx, "nginx", Options{UserMode: false}) bytes, err := GetMemoryUsage(ctx, "nginx", Options{UserMode: false})
@@ -231,13 +238,63 @@ func TestGetMemoryUsage(t *testing.T) {
}) })
} }
func TestGetUnits(t *testing.T) {
type testCase struct {
err error
runAsUser bool
opts Options
}
testCases := []testCase{{
// Run these tests only as a user
runAsUser: true,
opts: Options{UserMode: true},
err: nil,
}}
for _, tc := range testCases {
t.Run(fmt.Sprintf("as %s", userString), func(t *testing.T) {
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()
units, err := GetUnits(ctx, tc.opts)
if !errors.Is(err, tc.err) {
t.Errorf("error is %v, but should have been %v", err, tc.err)
}
if len(units) == 0 {
t.Errorf("Expected at least one unit, but got none")
}
unit := units[0]
if unit.Name == "" {
t.Errorf("Expected unit name to be non-empty, but got empty")
}
if unit.Load == "" {
t.Errorf("Expected unit load state to be non-empty, but got empty")
}
if unit.Active == "" {
t.Errorf("Expected unit active state to be non-empty, but got empty")
}
if unit.Sub == "" {
t.Errorf("Expected unit sub state to be non-empty, but got empty")
}
if unit.Description == "" {
t.Errorf("Expected unit description to be non-empty, but got empty")
}
})
}
}
func TestGetPID(t *testing.T) { func TestGetPID(t *testing.T) {
testCases := []struct { type testCase struct {
unit string unit string
err error err error
opts Options opts Options
runAsUser bool runAsUser bool
}{ }
testCases := []testCase{
// Run these tests only as a user // Run these tests only as a user
// try nonexistant unit in user mode as user // try nonexistant unit in user mode as user
@@ -257,22 +314,24 @@ func TestGetPID(t *testing.T) {
{"nginx", nil, Options{UserMode: false}, false}, {"nginx", nil, Options{UserMode: false}, false},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) { func(tc testCase) {
t.Parallel() t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
if (userString == "root" || userString == "system") && tc.runAsUser { t.Parallel()
t.Skip("skipping user test while running as superuser") if (userString == "root" || userString == "system") && tc.runAsUser {
} else if (userString != "root" && userString != "system") && !tc.runAsUser { t.Skip("skipping user test while running as superuser")
t.Skip("skipping superuser test while running as user") } 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() ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
_, err := GetPID(ctx, tc.unit, tc.opts) defer cancel()
if err != tc.err { _, err := GetPID(ctx, tc.unit, tc.opts)
t.Errorf("error is %v, but should have been %v", err, tc.err) if !errors.Is(err, tc.err) {
} t.Errorf("error is %v, but should have been %v", err, tc.err)
}) }
})
}(tc)
} }
t.Run(fmt.Sprintf("prove pid changes"), func(t *testing.T) { t.Run("prove pid changes", func(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping in short mode") t.Skip("skipping in short mode")
} }

View File

@@ -3,246 +3,330 @@ package properties
type Property string type Property string
const ( const (
ActiveEnterTimestamp Property = "ActiveEnterTimestamp" Accept Property = "Accept"
ActiveEnterTimestampMonotonic Property = "ActiveEnterTimestampMonotonic" ActiveEnterTimestamp Property = "ActiveEnterTimestamp"
ActiveExitTimestampMonotonic Property = "ActiveExitTimestampMonotonic" ActiveEnterTimestampMonotonic Property = "ActiveEnterTimestampMonotonic"
ActiveState Property = "ActiveState" ActiveExitTimestampMonotonic Property = "ActiveExitTimestampMonotonic"
After Property = "After" ActiveState Property = "ActiveState"
AllowIsolate Property = "AllowIsolate" After Property = "After"
AssertResult Property = "AssertResult" AllowIsolate Property = "AllowIsolate"
AssertTimestamp Property = "AssertTimestamp" AssertResult Property = "AssertResult"
AssertTimestampMonotonic Property = "AssertTimestampMonotonic" AssertTimestamp Property = "AssertTimestamp"
Before Property = "Before" AssertTimestampMonotonic Property = "AssertTimestampMonotonic"
BlockIOAccounting Property = "BlockIOAccounting" Backlog Property = "Backlog"
BlockIOWeight Property = "BlockIOWeight" Before Property = "Before"
CPUAccounting Property = "CPUAccounting" BindIPv6Only Property = "BindIPv6Only"
CPUAffinityFromNUMA Property = "CPUAffinityFromNUMA" BindLogSockets Property = "BindLogSockets"
CPUQuotaPerSecUSec Property = "CPUQuotaPerSecUSec" BlockIOAccounting Property = "BlockIOAccounting"
CPUQuotaPeriodUSec Property = "CPUQuotaPeriodUSec" BlockIOWeight Property = "BlockIOWeight"
CPUSchedulingPolicy Property = "CPUSchedulingPolicy" Broadcast Property = "Broadcast"
CPUSchedulingPriority Property = "CPUSchedulingPriority" CPUAccounting Property = "CPUAccounting"
CPUSchedulingResetOnFork Property = "CPUSchedulingResetOnFork" CPUAffinityFromNUMA Property = "CPUAffinityFromNUMA"
CPUShares Property = "CPUShares" CPUQuotaPerSecUSec Property = "CPUQuotaPerSecUSec"
CPUUsageNSec Property = "CPUUsageNSec" CPUQuotaPeriodUSec Property = "CPUQuotaPeriodUSec"
CPUWeight Property = "CPUWeight" CPUSchedulingPolicy Property = "CPUSchedulingPolicy"
CacheDirectoryMode Property = "CacheDirectoryMode" CPUSchedulingPriority Property = "CPUSchedulingPriority"
CanFreeze Property = "CanFreeze" CPUSchedulingResetOnFork Property = "CPUSchedulingResetOnFork"
CanIsolate Property = "CanIsolate" CPUShares Property = "CPUShares"
CanReload Property = "CanReload" CPUUsageNSec Property = "CPUUsageNSec"
CanStart Property = "CanStart" CPUWeight Property = "CPUWeight"
CanStop Property = "CanStop" CacheDirectoryMode Property = "CacheDirectoryMode"
CapabilityBoundingSet Property = "CapabilityBoundingSet" CanFreeze Property = "CanFreeze"
CleanResult Property = "CleanResult" CanIsolate Property = "CanIsolate"
CollectMode Property = "CollectMode" CanLiveMount Property = "CanLiveMount"
ConditionResult Property = "ConditionResult" CanReload Property = "CanReload"
ConditionTimestamp Property = "ConditionTimestamp" CanStart Property = "CanStart"
ConditionTimestampMonotonic Property = "ConditionTimestampMonotonic" CanStop Property = "CanStop"
ConfigurationDirectoryMode Property = "ConfigurationDirectoryMode" CapabilityBoundingSet Property = "CapabilityBoundingSet"
Conflicts Property = "Conflicts" CleanResult Property = "CleanResult"
ControlGroup Property = "ControlGroup" CollectMode Property = "CollectMode"
ControlPID Property = "ControlPID" ConditionResult Property = "ConditionResult"
CoredumpFilter Property = "CoredumpFilter" ConditionTimestamp Property = "ConditionTimestamp"
DefaultDependencies Property = "DefaultDependencies" ConditionTimestampMonotonic Property = "ConditionTimestampMonotonic"
DefaultMemoryLow Property = "DefaultMemoryLow" ConfigurationDirectoryMode Property = "ConfigurationDirectoryMode"
DefaultMemoryMin Property = "DefaultMemoryMin" Conflicts Property = "Conflicts"
Delegate Property = "Delegate" ControlGroup Property = "ControlGroup"
Description Property = "Description" ControlGroupId Property = "ControlGroupId"
DevicePolicy Property = "DevicePolicy" ControlPID Property = "ControlPID"
DynamicUser Property = "DynamicUser" CoredumpFilter Property = "CoredumpFilter"
EffectiveCPUs Property = "EffectiveCPUs" CoredumpReceive Property = "CoredumpReceive"
EffectiveMemoryNodes Property = "EffectiveMemoryNodes" DebugInvocation Property = "DebugInvocation"
ExecMainCode Property = "ExecMainCode" DefaultDependencies Property = "DefaultDependencies"
ExecMainExitTimestampMonotonic Property = "ExecMainExitTimestampMonotonic" DefaultMemoryLow Property = "DefaultMemoryLow"
ExecMainPID Property = "ExecMainPID" DefaultMemoryMin Property = "DefaultMemoryMin"
ExecMainStartTimestamp Property = "ExecMainStartTimestamp" DefaultStartupMemoryLow Property = "DefaultStartupMemoryLow"
ExecMainStartTimestampMonotonic Property = "ExecMainStartTimestampMonotonic" DeferAcceptUSec Property = "DeferAcceptUSec"
ExecMainStatus Property = "ExecMainStatus" Delegate Property = "Delegate"
ExecReload Property = "ExecReload" Description Property = "Description"
ExecReloadEx Property = "ExecReloadEx" DevicePolicy Property = "DevicePolicy"
ExecStart Property = "ExecStart" DirectoryMode Property = "DirectoryMode"
ExecStartEx Property = "ExecStartEx" DynamicUser Property = "DynamicUser"
FailureAction Property = "FailureAction" EffectiveCPUs Property = "EffectiveCPUs"
FileDescriptorStoreMax Property = "FileDescriptorStoreMax" EffectiveMemoryHigh Property = "EffectiveMemoryHigh"
FinalKillSignal Property = "FinalKillSignal" EffectiveMemoryMax Property = "EffectiveMemoryMax"
FragmentPath Property = "FragmentPath" EffectiveMemoryNodes Property = "EffectiveMemoryNodes"
FreezerState Property = "FreezerState" EffectiveTasksMax Property = "EffectiveTasksMax"
GID Property = "GID" ExecMainCode Property = "ExecMainCode"
GuessMainPID Property = "GuessMainPID" ExecMainExitTimestampMonotonic Property = "ExecMainExitTimestampMonotonic"
IOAccounting Property = "IOAccounting" ExecMainPID Property = "ExecMainPID"
IOReadBytes Property = "IOReadBytes" ExecMainStartTimestamp Property = "ExecMainStartTimestamp"
IOReadOperations Property = "IOReadOperations" ExecMainStartTimestampMonotonic Property = "ExecMainStartTimestampMonotonic"
IOSchedulingClass Property = "IOSchedulingClass" ExecMainStatus Property = "ExecMainStatus"
IOSchedulingPriority Property = "IOSchedulingPriority" ExecReload Property = "ExecReload"
IOWeight Property = "IOWeight" ExecReloadEx Property = "ExecReloadEx"
IOWriteBytes Property = "IOWriteBytes" ExecStart Property = "ExecStart"
IOWriteOperations Property = "IOWriteOperations" ExecStartEx Property = "ExecStartEx"
IPAccounting Property = "IPAccounting" ExtensionImagePolicy Property = "ExtensionImagePolicy"
IPEgressBytes Property = "IPEgressBytes" FailureAction Property = "FailureAction"
IPEgressPackets Property = "IPEgressPackets" FileDescriptorName Property = "FileDescriptorName"
IPIngressBytes Property = "IPIngressBytes" FileDescriptorStoreMax Property = "FileDescriptorStoreMax"
IPIngressPackets Property = "IPIngressPackets" FinalKillSignal Property = "FinalKillSignal"
Id Property = "Id" FlushPending Property = "FlushPending"
IgnoreOnIsolate Property = "IgnoreOnIsolate" FragmentPath Property = "FragmentPath"
IgnoreSIGPIPE Property = "IgnoreSIGPIPE" FreeBind Property = "FreeBind"
InactiveEnterTimestampMonotonic Property = "InactiveEnterTimestampMonotonic" FreezerState Property = "FreezerState"
InactiveExitTimestamp Property = "InactiveExitTimestamp" GID Property = "GID"
InactiveExitTimestampMonotonic Property = "InactiveExitTimestampMonotonic" GuessMainPID Property = "GuessMainPID"
InvocationID Property = "InvocationID" IOAccounting Property = "IOAccounting"
JobRunningTimeoutUSec Property = "JobRunningTimeoutUSec" IOReadBytes Property = "IOReadBytes"
JobTimeoutAction Property = "JobTimeoutAction" IOReadOperations Property = "IOReadOperations"
JobTimeoutUSec Property = "JobTimeoutUSec" IOSchedulingClass Property = "IOSchedulingClass"
KeyringMode Property = "KeyringMode" IOSchedulingPriority Property = "IOSchedulingPriority"
KillMode Property = "KillMode" IOWeight Property = "IOWeight"
KillSignal Property = "KillSignal" IOWriteBytes Property = "IOWriteBytes"
LimitAS Property = "LimitAS" IOWriteOperations Property = "IOWriteOperations"
LimitASSoft Property = "LimitASSoft" IPAccounting Property = "IPAccounting"
LimitCORE Property = "LimitCORE" IPEgressBytes Property = "IPEgressBytes"
LimitCORESoft Property = "LimitCORESoft" IPEgressPackets Property = "IPEgressPackets"
LimitCPU Property = "LimitCPU" IPIngressBytes Property = "IPIngressBytes"
LimitCPUSoft Property = "LimitCPUSoft" IPIngressPackets Property = "IPIngressPackets"
LimitDATA Property = "LimitDATA" IPTOS Property = "IPTOS"
LimitDATASoft Property = "LimitDATASoft" IPTTL Property = "IPTTL"
LimitFSIZE Property = "LimitFSIZE" Id Property = "Id"
LimitFSIZESoft Property = "LimitFSIZESoft" IgnoreOnIsolate Property = "IgnoreOnIsolate"
LimitLOCKS Property = "LimitLOCKS" IgnoreSIGPIPE Property = "IgnoreSIGPIPE"
LimitLOCKSSoft Property = "LimitLOCKSSoft" InactiveEnterTimestampMonotonic Property = "InactiveEnterTimestampMonotonic"
LimitMEMLOCK Property = "LimitMEMLOCK" InactiveExitTimestamp Property = "InactiveExitTimestamp"
LimitMEMLOCKSoft Property = "LimitMEMLOCKSoft" InactiveExitTimestampMonotonic Property = "InactiveExitTimestampMonotonic"
LimitMSGQUEUE Property = "LimitMSGQUEUE" InvocationID Property = "InvocationID"
LimitMSGQUEUESoft Property = "LimitMSGQUEUESoft" JobRunningTimeoutUSec Property = "JobRunningTimeoutUSec"
LimitNICE Property = "LimitNICE" JobTimeoutAction Property = "JobTimeoutAction"
LimitNICESoft Property = "LimitNICESoft" JobTimeoutUSec Property = "JobTimeoutUSec"
LimitNOFILE Property = "LimitNOFILE" KeepAlive Property = "KeepAlive"
LimitNOFILESoft Property = "LimitNOFILESoft" KeepAliveIntervalUSec Property = "KeepAliveIntervalUSec"
LimitNPROC Property = "LimitNPROC" KeepAliveProbes Property = "KeepAliveProbes"
LimitNPROCSoft Property = "LimitNPROCSoft" KeepAliveTimeUSec Property = "KeepAliveTimeUSec"
LimitRSS Property = "LimitRSS" KeyringMode Property = "KeyringMode"
LimitRSSSoft Property = "LimitRSSSoft" KillMode Property = "KillMode"
LimitRTPRIO Property = "LimitRTPRIO" KillSignal Property = "KillSignal"
LimitRTPRIOSoft Property = "LimitRTPRIOSoft" LimitAS Property = "LimitAS"
LimitRTTIME Property = "LimitRTTIME" LimitASSoft Property = "LimitASSoft"
LimitRTTIMESoft Property = "LimitRTTIMESoft" LimitCORE Property = "LimitCORE"
LimitSIGPENDING Property = "LimitSIGPENDING" LimitCORESoft Property = "LimitCORESoft"
LimitSIGPENDINGSoft Property = "LimitSIGPENDINGSoft" LimitCPU Property = "LimitCPU"
LimitSTACK Property = "LimitSTACK" LimitCPUSoft Property = "LimitCPUSoft"
LimitSTACKSoft Property = "LimitSTACKSoft" LimitDATA Property = "LimitDATA"
LoadState Property = "LoadState" LimitDATASoft Property = "LimitDATASoft"
LockPersonality Property = "LockPersonality" LimitFSIZE Property = "LimitFSIZE"
LogLevelMax Property = "LogLevelMax" LimitFSIZESoft Property = "LimitFSIZESoft"
LogRateLimitBurst Property = "LogRateLimitBurst" LimitLOCKS Property = "LimitLOCKS"
LogRateLimitIntervalUSec Property = "LogRateLimitIntervalUSec" LimitLOCKSSoft Property = "LimitLOCKSSoft"
LogsDirectoryMode Property = "LogsDirectoryMode" LimitMEMLOCK Property = "LimitMEMLOCK"
MainPID Property = "MainPID" LimitMEMLOCKSoft Property = "LimitMEMLOCKSoft"
ManagedOOMMemoryPressure Property = "ManagedOOMMemoryPressure" LimitMSGQUEUE Property = "LimitMSGQUEUE"
ManagedOOMMemoryPressureLimit Property = "ManagedOOMMemoryPressureLimit" LimitMSGQUEUESoft Property = "LimitMSGQUEUESoft"
ManagedOOMPreference Property = "ManagedOOMPreference" LimitNICE Property = "LimitNICE"
ManagedOOMSwap Property = "ManagedOOMSwap" LimitNICESoft Property = "LimitNICESoft"
MemoryAccounting Property = "MemoryAccounting" LimitNOFILE Property = "LimitNOFILE"
MemoryCurrent Property = "MemoryCurrent" LimitNOFILESoft Property = "LimitNOFILESoft"
MemoryDenyWriteExecute Property = "MemoryDenyWriteExecute" LimitNPROC Property = "LimitNPROC"
MemoryHigh Property = "MemoryHigh" LimitNPROCSoft Property = "LimitNPROCSoft"
MemoryLimit Property = "MemoryLimit" LimitRSS Property = "LimitRSS"
MemoryLow Property = "MemoryLow" LimitRSSSoft Property = "LimitRSSSoft"
MemoryMax Property = "MemoryMax" LimitRTPRIO Property = "LimitRTPRIO"
MemoryMin Property = "MemoryMin" LimitRTPRIOSoft Property = "LimitRTPRIOSoft"
MemorySwapMax Property = "MemorySwapMax" LimitRTTIME Property = "LimitRTTIME"
MountAPIVFS Property = "MountAPIVFS" LimitRTTIMESoft Property = "LimitRTTIMESoft"
NFileDescriptorStore Property = "NFileDescriptorStore" LimitSIGPENDING Property = "LimitSIGPENDING"
NRestarts Property = "NRestarts" LimitSIGPENDINGSoft Property = "LimitSIGPENDINGSoft"
NUMAPolicy Property = "NUMAPolicy" LimitSTACK Property = "LimitSTACK"
Names Property = "Names" LimitSTACKSoft Property = "LimitSTACKSoft"
NeedDaemonReload Property = "NeedDaemonReload" Listen Property = "Listen"
Nice Property = "Nice" LoadState Property = "LoadState"
NoNewPrivileges Property = "NoNewPrivileges" LockPersonality Property = "LockPersonality"
NonBlocking Property = "NonBlocking" LogLevelMax Property = "LogLevelMax"
NotifyAccess Property = "NotifyAccess" LogRateLimitBurst Property = "LogRateLimitBurst"
OOMPolicy Property = "OOMPolicy" LogRateLimitIntervalUSec Property = "LogRateLimitIntervalUSec"
OOMScoreAdjust Property = "OOMScoreAdjust" LogsDirectoryMode Property = "LogsDirectoryMode"
OnFailureJobMode Property = "OnFailureJobMode" MainPID Property = "MainPID"
PIDFile Property = "PIDFile" ManagedOOMMemoryPressure Property = "ManagedOOMMemoryPressure"
Perpetual Property = "Perpetual" ManagedOOMMemoryPressureDurationUSec Property = "ManagedOOMMemoryPressureDurationUSec"
PrivateDevices Property = "PrivateDevices" ManagedOOMMemoryPressureLimit Property = "ManagedOOMMemoryPressureLimit"
PrivateIPC Property = "PrivateIPC" ManagedOOMPreference Property = "ManagedOOMPreference"
PrivateMounts Property = "PrivateMounts" ManagedOOMSwap Property = "ManagedOOMSwap"
PrivateNetwork Property = "PrivateNetwork" Mark Property = "Mark"
PrivateTmp Property = "PrivateTmp" MaxConnections Property = "MaxConnections"
PrivateUsers Property = "PrivateUsers" MaxConnectionsPerSource Property = "MaxConnectionsPerSource"
ProcSubset Property = "ProcSubset" MemoryAccounting Property = "MemoryAccounting"
ProtectClock Property = "ProtectClock" MemoryAvailable Property = "MemoryAvailable"
ProtectControlGroups Property = "ProtectControlGroups" MemoryCurrent Property = "MemoryCurrent"
ProtectHome Property = "ProtectHome" MemoryDenyWriteExecute Property = "MemoryDenyWriteExecute"
ProtectHostname Property = "ProtectHostname" MemoryHigh Property = "MemoryHigh"
ProtectKernelLogs Property = "ProtectKernelLogs" MemoryKSM Property = "MemoryKSM"
ProtectKernelModules Property = "ProtectKernelModules" MemoryLimit Property = "MemoryLimit"
ProtectKernelTunables Property = "ProtectKernelTunables" MemoryLow Property = "MemoryLow"
ProtectProc Property = "ProtectProc" MemoryMax Property = "MemoryMax"
ProtectSystem Property = "ProtectSystem" MemoryMin Property = "MemoryMin"
RefuseManualStart Property = "RefuseManualStart" MemoryPeak Property = "MemoryPeak"
RefuseManualStop Property = "RefuseManualStop" MemoryPressureThresholdUSec Property = "MemoryPressureThresholdUSec"
ReloadResult Property = "ReloadResult" MemoryPressureWatch Property = "MemoryPressureWatch"
RemainAfterExit Property = "RemainAfterExit" MemorySwapCurrent Property = "MemorySwapCurrent"
RemoveIPC Property = "RemoveIPC" MemorySwapMax Property = "MemorySwapMax"
Requires Property = "Requires" MemorySwapPeak Property = "MemorySwapPeak"
Restart Property = "Restart" MemoryZSwapCurrent Property = "MemoryZSwapCurrent"
RestartKillSignal Property = "RestartKillSignal" MemoryZSwapMax Property = "MemoryZSwapMax"
RestartUSec Property = "RestartUSec" MemoryZSwapWriteback Property = "MemoryZSwapWriteback"
RestrictNamespaces Property = "RestrictNamespaces" MessageQueueMaxMessages Property = "MessageQueueMaxMessages"
RestrictRealtime Property = "RestrictRealtime" MessageQueueMessageSize Property = "MessageQueueMessageSize"
RestrictSUIDSGID Property = "RestrictSUIDSGID" MountAPIVFS Property = "MountAPIVFS"
Result Property = "Result" MountImagePolicy Property = "MountImagePolicy"
RootDirectoryStartOnly Property = "RootDirectoryStartOnly" NAccepted Property = "NAccepted"
RuntimeDirectoryMode Property = "RuntimeDirectoryMode" NConnections Property = "NConnections"
RuntimeDirectoryPreserve Property = "RuntimeDirectoryPreserve" NFileDescriptorStore Property = "NFileDescriptorStore"
RuntimeMaxUSec Property = "RuntimeMaxUSec" NRefused Property = "NRefused"
SameProcessGroup Property = "SameProcessGroup" NRestarts Property = "NRestarts"
SecureBits Property = "SecureBits" NUMAPolicy Property = "NUMAPolicy"
SendSIGHUP Property = "SendSIGHUP" Names Property = "Names"
SendSIGKILL Property = "SendSIGKILL" NeedDaemonReload Property = "NeedDaemonReload"
Slice Property = "Slice" Nice Property = "Nice"
StandardError Property = "StandardError" NoDelay Property = "NoDelay"
StandardInput Property = "StandardInput" NoNewPrivileges Property = "NoNewPrivileges"
StandardOutput Property = "StandardOutput" NonBlocking Property = "NonBlocking"
StartLimitAction Property = "StartLimitAction" NotifyAccess Property = "NotifyAccess"
StartLimitBurst Property = "StartLimitBurst" OOMPolicy Property = "OOMPolicy"
StartLimitIntervalUSec Property = "StartLimitIntervalUSec" OOMScoreAdjust Property = "OOMScoreAdjust"
StartupBlockIOWeight Property = "StartupBlockIOWeight" OnFailureJobMode Property = "OnFailureJobMode"
StartupCPUShares Property = "StartupCPUShares" OnSuccessJobMode Property = "OnSuccessJobMode"
StartupCPUWeight Property = "StartupCPUWeight" PIDFile Property = "PIDFile"
StartupIOWeight Property = "StartupIOWeight" PassCredentials Property = "PassCredentials"
StateChangeTimestamp Property = "StateChangeTimestamp" PassFileDescriptorsToExec Property = "PassFileDescriptorsToExec"
StateChangeTimestampMonotonic Property = "StateChangeTimestampMonotonic" PassPacketInfo Property = "PassPacketInfo"
StateDirectoryMode Property = "StateDirectoryMode" PassSecurity Property = "PassSecurity"
StatusErrno Property = "StatusErrno" Perpetual Property = "Perpetual"
StopWhenUnneeded Property = "StopWhenUnneeded" PipeSize Property = "PipeSize"
SubState Property = "SubState" PollLimitBurst Property = "PollLimitBurst"
SuccessAction Property = "SuccessAction" PollLimitIntervalUSec Property = "PollLimitIntervalUSec"
SyslogFacility Property = "SyslogFacility" Priority Property = "Priority"
SyslogLevel Property = "SyslogLevel" PrivateDevices Property = "PrivateDevices"
SyslogLevelPrefix Property = "SyslogLevelPrefix" PrivateIPC Property = "PrivateIPC"
SyslogPriority Property = "SyslogPriority" PrivateMounts Property = "PrivateMounts"
SystemCallErrorNumber Property = "SystemCallErrorNumber" PrivateNetwork Property = "PrivateNetwork"
TTYReset Property = "TTYReset" PrivatePIDs Property = "PrivatePIDs"
TTYVHangup Property = "TTYVHangup" PrivateTmp Property = "PrivateTmp"
TTYVTDisallocate Property = "TTYVTDisallocate" PrivateTmpEx Property = "PrivateTmpEx"
TasksAccounting Property = "TasksAccounting" PrivateUsers Property = "PrivateUsers"
TasksCurrent Property = "TasksCurrent" PrivateUsersEx Property = "PrivateUsersEx"
TasksMax Property = "TasksMax" ProcSubset Property = "ProcSubset"
TimeoutAbortUSec Property = "TimeoutAbortUSec" ProtectClock Property = "ProtectClock"
TimeoutCleanUSec Property = "TimeoutCleanUSec" ProtectControlGroups Property = "ProtectControlGroups"
TimeoutStartFailureMode Property = "TimeoutStartFailureMode" ProtectControlGroupsEx Property = "ProtectControlGroupsEx"
TimeoutStartUSec Property = "TimeoutStartUSec" ProtectHome Property = "ProtectHome"
TimeoutStopFailureMode Property = "TimeoutStopFailureMode" ProtectHostname Property = "ProtectHostname"
TimeoutStopUSec Property = "TimeoutStopUSec" ProtectKernelLogs Property = "ProtectKernelLogs"
TimerSlackNSec Property = "TimerSlackNSec" ProtectKernelModules Property = "ProtectKernelModules"
Transient Property = "Transient" ProtectKernelTunables Property = "ProtectKernelTunables"
Type Property = "Type" ProtectProc Property = "ProtectProc"
UID Property = "UID" ProtectSystem Property = "ProtectSystem"
UMask Property = "UMask" ReceiveBuffer Property = "ReceiveBuffer"
UnitFilePreset Property = "UnitFilePreset" RefuseManualStart Property = "RefuseManualStart"
UnitFileState Property = "UnitFileState" RefuseManualStop Property = "RefuseManualStop"
UtmpMode Property = "UtmpMode" ReloadResult Property = "ReloadResult"
WantedBy Property = "WantedBy" RemainAfterExit Property = "RemainAfterExit"
WatchdogSignal Property = "WatchdogSignal" RemoveIPC Property = "RemoveIPC"
WatchdogTimestampMonotonic Property = "WatchdogTimestampMonotonic" RemoveOnStop Property = "RemoveOnStop"
WatchdogUSec Property = "WatchdogUSec" RequiredBy Property = "RequiredBy"
Requires Property = "Requires"
RequiresMountsFor Property = "RequiresMountsFor"
Restart Property = "Restart"
RestartKillSignal Property = "RestartKillSignal"
RestartUSec Property = "RestartUSec"
RestrictNamespaces Property = "RestrictNamespaces"
RestrictRealtime Property = "RestrictRealtime"
RestrictSUIDSGID Property = "RestrictSUIDSGID"
Result Property = "Result"
ReusePort Property = "ReusePort"
RootDirectoryStartOnly Property = "RootDirectoryStartOnly"
RootEphemeral Property = "RootEphemeral"
RootImagePolicy Property = "RootImagePolicy"
RuntimeDirectoryMode Property = "RuntimeDirectoryMode"
RuntimeDirectoryPreserve Property = "RuntimeDirectoryPreserve"
RuntimeMaxUSec Property = "RuntimeMaxUSec"
SameProcessGroup Property = "SameProcessGroup"
SecureBits Property = "SecureBits"
SendBuffer Property = "SendBuffer"
SendSIGHUP Property = "SendSIGHUP"
SendSIGKILL Property = "SendSIGKILL"
SetLoginEnvironment Property = "SetLoginEnvironment"
Slice Property = "Slice"
SocketMode Property = "SocketMode"
SocketProtocol Property = "SocketProtocol"
StandardError Property = "StandardError"
StandardInput Property = "StandardInput"
StandardOutput Property = "StandardOutput"
StartLimitAction Property = "StartLimitAction"
StartLimitBurst Property = "StartLimitBurst"
StartLimitIntervalUSec Property = "StartLimitIntervalUSec"
StartupBlockIOWeight Property = "StartupBlockIOWeight"
StartupCPUShares Property = "StartupCPUShares"
StartupCPUWeight Property = "StartupCPUWeight"
StartupIOWeight Property = "StartupIOWeight"
StartupMemoryHigh Property = "StartupMemoryHigh"
StartupMemoryLow Property = "StartupMemoryLow"
StartupMemoryMax Property = "StartupMemoryMax"
StartupMemorySwapMax Property = "StartupMemorySwapMax"
StartupMemoryZSwapMax Property = "StartupMemoryZSwapMax"
StateChangeTimestamp Property = "StateChangeTimestamp"
StateChangeTimestampMonotonic Property = "StateChangeTimestampMonotonic"
StateDirectoryMode Property = "StateDirectoryMode"
StatusErrno Property = "StatusErrno"
StopWhenUnneeded Property = "StopWhenUnneeded"
SubState Property = "SubState"
SuccessAction Property = "SuccessAction"
SurviveFinalKillSignal Property = "SurviveFinalKillSignal"
SyslogFacility Property = "SyslogFacility"
SyslogLevel Property = "SyslogLevel"
SyslogLevelPrefix Property = "SyslogLevelPrefix"
SyslogPriority Property = "SyslogPriority"
SystemCallErrorNumber Property = "SystemCallErrorNumber"
TTYReset Property = "TTYReset"
TTYVHangup Property = "TTYVHangup"
TTYVTDisallocate Property = "TTYVTDisallocate"
TasksAccounting Property = "TasksAccounting"
TasksCurrent Property = "TasksCurrent"
TasksMax Property = "TasksMax"
TimeoutAbortUSec Property = "TimeoutAbortUSec"
TimeoutCleanUSec Property = "TimeoutCleanUSec"
TimeoutStartFailureMode Property = "TimeoutStartFailureMode"
TimeoutStartUSec Property = "TimeoutStartUSec"
TimeoutStopFailureMode Property = "TimeoutStopFailureMode"
TimeoutStopUSec Property = "TimeoutStopUSec"
TimeoutUSec Property = "TimeoutUSec"
TimerSlackNSec Property = "TimerSlackNSec"
Timestamping Property = "Timestamping"
Transient Property = "Transient"
Transparent Property = "Transparent"
TriggerLimitBurst Property = "TriggerLimitBurst"
TriggerLimitIntervalUSec Property = "TriggerLimitIntervalUSec"
Triggers Property = "Triggers"
Type Property = "Type"
UID Property = "UID"
UMask Property = "UMask"
UnitFilePreset Property = "UnitFilePreset"
UnitFileState Property = "UnitFileState"
UtmpMode Property = "UtmpMode"
WantedBy Property = "WantedBy"
WatchdogSignal Property = "WatchdogSignal"
WatchdogTimestampMonotonic Property = "WatchdogTimestampMonotonic"
WatchdogUSec Property = "WatchdogUSec"
Writable Property = "Writable"
) )

View File

@@ -1,6 +1,7 @@
package properties package properties
var Properties = []Property{ var Properties = []Property{
Accept,
ActiveEnterTimestamp, ActiveEnterTimestamp,
ActiveEnterTimestampMonotonic, ActiveEnterTimestampMonotonic,
ActiveExitTimestampMonotonic, ActiveExitTimestampMonotonic,
@@ -10,9 +11,13 @@ var Properties = []Property{
AssertResult, AssertResult,
AssertTimestamp, AssertTimestamp,
AssertTimestampMonotonic, AssertTimestampMonotonic,
Backlog,
Before, Before,
BindIPv6Only,
BindLogSockets,
BlockIOAccounting, BlockIOAccounting,
BlockIOWeight, BlockIOWeight,
Broadcast,
CPUAccounting, CPUAccounting,
CPUAffinityFromNUMA, CPUAffinityFromNUMA,
CPUQuotaPerSecUSec, CPUQuotaPerSecUSec,
@@ -26,6 +31,7 @@ var Properties = []Property{
CacheDirectoryMode, CacheDirectoryMode,
CanFreeze, CanFreeze,
CanIsolate, CanIsolate,
CanLiveMount,
CanReload, CanReload,
CanStart, CanStart,
CanStop, CanStop,
@@ -38,17 +44,26 @@ var Properties = []Property{
ConfigurationDirectoryMode, ConfigurationDirectoryMode,
Conflicts, Conflicts,
ControlGroup, ControlGroup,
ControlGroupId,
ControlPID, ControlPID,
CoredumpFilter, CoredumpFilter,
CoredumpReceive,
DebugInvocation,
DefaultDependencies, DefaultDependencies,
DefaultMemoryLow, DefaultMemoryLow,
DefaultMemoryMin, DefaultMemoryMin,
DefaultStartupMemoryLow,
DeferAcceptUSec,
Delegate, Delegate,
Description, Description,
DevicePolicy, DevicePolicy,
DirectoryMode,
DynamicUser, DynamicUser,
EffectiveCPUs, EffectiveCPUs,
EffectiveMemoryHigh,
EffectiveMemoryMax,
EffectiveMemoryNodes, EffectiveMemoryNodes,
EffectiveTasksMax,
ExecMainCode, ExecMainCode,
ExecMainExitTimestampMonotonic, ExecMainExitTimestampMonotonic,
ExecMainPID, ExecMainPID,
@@ -59,10 +74,14 @@ var Properties = []Property{
ExecReloadEx, ExecReloadEx,
ExecStart, ExecStart,
ExecStartEx, ExecStartEx,
ExtensionImagePolicy,
FailureAction, FailureAction,
FileDescriptorName,
FileDescriptorStoreMax, FileDescriptorStoreMax,
FinalKillSignal, FinalKillSignal,
FlushPending,
FragmentPath, FragmentPath,
FreeBind,
FreezerState, FreezerState,
GID, GID,
GuessMainPID, GuessMainPID,
@@ -79,6 +98,8 @@ var Properties = []Property{
IPEgressPackets, IPEgressPackets,
IPIngressBytes, IPIngressBytes,
IPIngressPackets, IPIngressPackets,
IPTOS,
IPTTL,
Id, Id,
IgnoreOnIsolate, IgnoreOnIsolate,
IgnoreSIGPIPE, IgnoreSIGPIPE,
@@ -89,6 +110,10 @@ var Properties = []Property{
JobRunningTimeoutUSec, JobRunningTimeoutUSec,
JobTimeoutAction, JobTimeoutAction,
JobTimeoutUSec, JobTimeoutUSec,
KeepAlive,
KeepAliveIntervalUSec,
KeepAliveProbes,
KeepAliveTimeUSec,
KeyringMode, KeyringMode,
KillMode, KillMode,
KillSignal, KillSignal,
@@ -124,6 +149,7 @@ var Properties = []Property{
LimitSIGPENDINGSoft, LimitSIGPENDINGSoft,
LimitSTACK, LimitSTACK,
LimitSTACKSoft, LimitSTACKSoft,
Listen,
LoadState, LoadState,
LockPersonality, LockPersonality,
LogLevelMax, LogLevelMax,
@@ -132,42 +158,76 @@ var Properties = []Property{
LogsDirectoryMode, LogsDirectoryMode,
MainPID, MainPID,
ManagedOOMMemoryPressure, ManagedOOMMemoryPressure,
ManagedOOMMemoryPressureDurationUSec,
ManagedOOMMemoryPressureLimit, ManagedOOMMemoryPressureLimit,
ManagedOOMPreference, ManagedOOMPreference,
ManagedOOMSwap, ManagedOOMSwap,
Mark,
MaxConnections,
MaxConnectionsPerSource,
MemoryAccounting, MemoryAccounting,
MemoryAvailable,
MemoryCurrent, MemoryCurrent,
MemoryDenyWriteExecute, MemoryDenyWriteExecute,
MemoryHigh, MemoryHigh,
MemoryKSM,
MemoryLimit, MemoryLimit,
MemoryLow, MemoryLow,
MemoryMax, MemoryMax,
MemoryMin, MemoryMin,
MemoryPeak,
MemoryPressureThresholdUSec,
MemoryPressureWatch,
MemorySwapCurrent,
MemorySwapMax, MemorySwapMax,
MemorySwapPeak,
MemoryZSwapCurrent,
MemoryZSwapMax,
MemoryZSwapWriteback,
MessageQueueMaxMessages,
MessageQueueMessageSize,
MountAPIVFS, MountAPIVFS,
MountImagePolicy,
NAccepted,
NConnections,
NFileDescriptorStore, NFileDescriptorStore,
NRefused,
NRestarts, NRestarts,
NUMAPolicy, NUMAPolicy,
Names, Names,
NeedDaemonReload, NeedDaemonReload,
Nice, Nice,
NoDelay,
NoNewPrivileges, NoNewPrivileges,
NonBlocking, NonBlocking,
NotifyAccess, NotifyAccess,
OOMPolicy, OOMPolicy,
OOMScoreAdjust, OOMScoreAdjust,
OnFailureJobMode, OnFailureJobMode,
OnSuccessJobMode,
PIDFile, PIDFile,
PassCredentials,
PassFileDescriptorsToExec,
PassPacketInfo,
PassSecurity,
Perpetual, Perpetual,
PipeSize,
PollLimitBurst,
PollLimitIntervalUSec,
Priority,
PrivateDevices, PrivateDevices,
PrivateIPC, PrivateIPC,
PrivateMounts, PrivateMounts,
PrivateNetwork, PrivateNetwork,
PrivatePIDs,
PrivateTmp, PrivateTmp,
PrivateTmpEx,
PrivateUsers, PrivateUsers,
PrivateUsersEx,
ProcSubset, ProcSubset,
ProtectClock, ProtectClock,
ProtectControlGroups, ProtectControlGroups,
ProtectControlGroupsEx,
ProtectHome, ProtectHome,
ProtectHostname, ProtectHostname,
ProtectKernelLogs, ProtectKernelLogs,
@@ -175,12 +235,16 @@ var Properties = []Property{
ProtectKernelTunables, ProtectKernelTunables,
ProtectProc, ProtectProc,
ProtectSystem, ProtectSystem,
ReceiveBuffer,
RefuseManualStart, RefuseManualStart,
RefuseManualStop, RefuseManualStop,
ReloadResult, ReloadResult,
RemainAfterExit, RemainAfterExit,
RemoveIPC, RemoveIPC,
RemoveOnStop,
RequiredBy,
Requires, Requires,
RequiresMountsFor,
Restart, Restart,
RestartKillSignal, RestartKillSignal,
RestartUSec, RestartUSec,
@@ -188,15 +252,22 @@ var Properties = []Property{
RestrictRealtime, RestrictRealtime,
RestrictSUIDSGID, RestrictSUIDSGID,
Result, Result,
ReusePort,
RootDirectoryStartOnly, RootDirectoryStartOnly,
RootEphemeral,
RootImagePolicy,
RuntimeDirectoryMode, RuntimeDirectoryMode,
RuntimeDirectoryPreserve, RuntimeDirectoryPreserve,
RuntimeMaxUSec, RuntimeMaxUSec,
SameProcessGroup, SameProcessGroup,
SecureBits, SecureBits,
SendBuffer,
SendSIGHUP, SendSIGHUP,
SendSIGKILL, SendSIGKILL,
SetLoginEnvironment,
Slice, Slice,
SocketMode,
SocketProtocol,
StandardError, StandardError,
StandardInput, StandardInput,
StandardOutput, StandardOutput,
@@ -207,6 +278,11 @@ var Properties = []Property{
StartupCPUShares, StartupCPUShares,
StartupCPUWeight, StartupCPUWeight,
StartupIOWeight, StartupIOWeight,
StartupMemoryHigh,
StartupMemoryLow,
StartupMemoryMax,
StartupMemorySwapMax,
StartupMemoryZSwapMax,
StateChangeTimestamp, StateChangeTimestamp,
StateChangeTimestampMonotonic, StateChangeTimestampMonotonic,
StateDirectoryMode, StateDirectoryMode,
@@ -214,6 +290,7 @@ var Properties = []Property{
StopWhenUnneeded, StopWhenUnneeded,
SubState, SubState,
SuccessAction, SuccessAction,
SurviveFinalKillSignal,
SyslogFacility, SyslogFacility,
SyslogLevel, SyslogLevel,
SyslogLevelPrefix, SyslogLevelPrefix,
@@ -231,8 +308,14 @@ var Properties = []Property{
TimeoutStartUSec, TimeoutStartUSec,
TimeoutStopFailureMode, TimeoutStopFailureMode,
TimeoutStopUSec, TimeoutStopUSec,
TimeoutUSec,
TimerSlackNSec, TimerSlackNSec,
Timestamping,
Transient, Transient,
Transparent,
TriggerLimitBurst,
TriggerLimitIntervalUSec,
Triggers,
Type, Type,
UID, UID,
UMask, UMask,
@@ -243,4 +326,5 @@ var Properties = []Property{
WatchdogSignal, WatchdogSignal,
WatchdogTimestampMonotonic, WatchdogTimestampMonotonic,
WatchdogUSec, WatchdogUSec,
Writable,
} }

View File

@@ -1,5 +1,42 @@
package systemctl package systemctl
import "strings"
type Options struct { type Options struct {
UserMode bool UserMode bool
} }
type Unit struct {
Name string
Load string
Active string
Sub string
Description string
}
// UnitTypes contains all valid systemd unit type suffixes.
var UnitTypes = []string{
"automount",
"device",
"mount",
"path",
"scope",
"service",
"slice",
"snapshot",
"socket",
"swap",
"target",
"timer",
}
// HasValidUnitSuffix checks whether the given unit name ends with a valid
// systemd unit type suffix (e.g. ".service", ".timer").
func HasValidUnitSuffix(unit string) bool {
for _, t := range UnitTypes {
if strings.HasSuffix(unit, "."+t) {
return true
}
}
return false
}

View File

@@ -2,8 +2,6 @@ package systemctl
import ( import (
"context" "context"
"regexp"
"strings"
"github.com/taigrr/systemctl/properties" "github.com/taigrr/systemctl/properties"
) )
@@ -14,13 +12,10 @@ import (
// files, and recreate the entire dependency tree. While the daemon is being // files, and recreate the entire dependency tree. While the daemon is being
// reloaded, all sockets systemd listens on behalf of user configuration will // reloaded, all sockets systemd listens on behalf of user configuration will
// stay accessible. // stay accessible.
func DaemonReload(ctx context.Context, opts Options) error { //
args := []string{"daemon-reload", "--system"} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func DaemonReload(ctx context.Context, opts Options, args ...string) error {
args[1] = "--user" return daemonReload(ctx, opts, args...)
}
_, _, _, err := execute(ctx, args)
return err
} }
// Reenables one or more units. // Reenables one or more units.
@@ -28,13 +23,10 @@ func DaemonReload(ctx context.Context, opts Options) error {
// This removes all symlinks to the unit files backing the specified units from // This removes all symlinks to the unit files backing the specified units from
// the unit configuration directory, then recreates the symlink to the unit again, // the unit configuration directory, then recreates the symlink to the unit again,
// atomically. Can be used to change the symlink target. // atomically. Can be used to change the symlink target.
func Reenable(ctx context.Context, unit string, opts Options) error { //
args := []string{"reenable", "--system", unit} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func Reenable(ctx context.Context, unit string, opts Options, args ...string) error {
args[1] = "--user" return reenable(ctx, unit, opts, args...)
}
_, _, _, err := execute(ctx, args)
return err
} }
// Disables one or more units. // Disables one or more units.
@@ -42,13 +34,10 @@ func Reenable(ctx context.Context, unit string, opts Options) error {
// This removes all symlinks to the unit files backing the specified units from // This removes all symlinks to the unit files backing the specified units from
// the unit configuration directory, and hence undoes any changes made by // the unit configuration directory, and hence undoes any changes made by
// enable or link. // enable or link.
func Disable(ctx context.Context, unit string, opts Options) error { //
args := []string{"disable", "--system", unit} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func Disable(ctx context.Context, unit string, opts Options, args ...string) error {
args[1] = "--user" return disable(ctx, unit, opts, args...)
}
_, _, _, err := execute(ctx, args)
return err
} }
// Enable one or more units or unit instances. // Enable one or more units or unit instances.
@@ -57,38 +46,20 @@ func Disable(ctx context.Context, unit string, opts Options) error {
// the indicated unit files. After the symlinks have been created, the system // the indicated unit files. After the symlinks have been created, the system
// manager configuration is reloaded (in a way equivalent to daemon-reload), // manager configuration is reloaded (in a way equivalent to daemon-reload),
// in order to ensure the changes are taken into account immediately. // in order to ensure the changes are taken into account immediately.
func Enable(ctx context.Context, unit string, opts Options) error { //
args := []string{"enable", "--system", unit} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func Enable(ctx context.Context, unit string, opts Options, args ...string) error {
args[1] = "--user" return enable(ctx, unit, opts, args...)
}
_, _, _, err := execute(ctx, args)
return err
} }
// Check whether any of the specified units are active (i.e. running). // Check whether any of the specified units are active (i.e. running).
// //
// Returns true if the unit is active, false if inactive or failed. // Returns true if the unit is active, false if inactive or failed.
// Also returns false in an error case. // Also returns false in an error case.
func IsActive(ctx context.Context, unit string, opts Options) (bool, error) { //
args := []string{"is-active", "--system", unit} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func IsActive(ctx context.Context, unit string, opts Options, args ...string) (bool, error) {
args[1] = "--user" return isActive(ctx, unit, opts, args...)
}
stdout, _, _, err := execute(ctx, args)
stdout = strings.TrimSuffix(stdout, "\n")
switch stdout {
case "inactive":
return false, nil
case "active":
return true, nil
case "failed":
return false, nil
case "activating":
return false, nil
default:
return false, err
}
} }
// Checks whether any of the specified unit files are enabled (as with enable). // Checks whether any of the specified unit files are enabled (as with enable).
@@ -100,60 +71,17 @@ func IsActive(ctx context.Context, unit string, opts Options) (bool, error) {
// //
// See https://www.freedesktop.org/software/systemd/man/systemctl.html#is-enabled%20UNIT%E2%80%A6 // See https://www.freedesktop.org/software/systemd/man/systemctl.html#is-enabled%20UNIT%E2%80%A6
// for more information // for more information
func IsEnabled(ctx context.Context, unit string, opts Options) (bool, error) { //
args := []string{"is-enabled", "--system", unit} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func IsEnabled(ctx context.Context, unit string, opts Options, args ...string) (bool, error) {
args[1] = "--user" return isEnabled(ctx, unit, opts, args...)
}
stdout, _, _, err := execute(ctx, args)
stdout = strings.TrimSuffix(stdout, "\n")
switch stdout {
case "enabled":
return true, nil
case "enabled-runtime":
return true, nil
case "linked":
return false, ErrLinked
case "linked-runtime":
return false, ErrLinked
case "alias":
return true, nil
case "masked":
return false, ErrMasked
case "masked-runtime":
return false, ErrMasked
case "static":
return true, nil
case "indirect":
return true, nil
case "disabled":
return false, nil
case "generated":
return true, nil
case "transient":
return true, nil
}
if err != nil {
return false, err
}
return false, ErrUnspecified
} }
// Check whether any of the specified units are in a "failed" state. // Check whether any of the specified units are in a "failed" state.
func IsFailed(ctx context.Context, unit string, opts Options) (bool, error) { //
args := []string{"is-failed", "--system", unit} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func IsFailed(ctx context.Context, unit string, opts Options, args ...string) (bool, error) {
args[1] = "--user" return isFailed(ctx, unit, opts, args...)
}
stdout, _, _, err := execute(ctx, args)
if matched, _ := regexp.MatchString(`inactive`, stdout); matched {
return false, nil
} else if matched, _ := regexp.MatchString(`active`, stdout); matched {
return false, nil
} else if matched, _ := regexp.MatchString(`failed`, stdout); matched {
return true, nil
}
return false, err
} }
// Mask one or more units, as specified on the command line. This will link // Mask one or more units, as specified on the command line. This will link
@@ -162,71 +90,51 @@ func IsFailed(ctx context.Context, unit string, opts Options) (bool, error) {
// Notably, Mask may return ErrDoesNotExist if a unit doesn't exist, but it will // Notably, Mask may return ErrDoesNotExist if a unit doesn't exist, but it will
// continue masking anyway. Calling Mask on a non-existing masked unit does not // continue masking anyway. Calling Mask on a non-existing masked unit does not
// return an error. Similarly, see Unmask. // return an error. Similarly, see Unmask.
func Mask(ctx context.Context, unit string, opts Options) error { //
args := []string{"mask", "--system", unit} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func Mask(ctx context.Context, unit string, opts Options, args ...string) error {
args[1] = "--user" return mask(ctx, unit, opts, args...)
}
_, _, _, err := execute(ctx, args)
return err
} }
// Stop and then start one or more units specified on the command line. // Stop and then start one or more units specified on the command line.
// If the units are not running yet, they will be started. // If the units are not running yet, they will be started.
func Restart(ctx context.Context, unit string, opts Options) error { //
args := []string{"restart", "--system", unit} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func Restart(ctx context.Context, unit string, opts Options, args ...string) error {
args[1] = "--user" return restart(ctx, unit, opts, args...)
}
_, _, _, err := execute(ctx, args)
return err
} }
// Show a selected property of a unit. Accepted properties are predefined in the // Show a selected property of a unit. Accepted properties are predefined in the
// properties subpackage to guarantee properties are valid and assist code-completion. // properties subpackage to guarantee properties are valid and assist code-completion.
func Show(ctx context.Context, unit string, property properties.Property, opts Options) (string, error) { //
args := []string{"show", "--system", unit, "--property", string(property)} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func Show(ctx context.Context, unit string, property properties.Property, opts Options, args ...string) (string, error) {
args[1] = "--user" return show(ctx, unit, property, opts, args...)
}
stdout, _, _, err := execute(ctx, args)
stdout = strings.TrimPrefix(stdout, string(property)+"=")
stdout = strings.TrimSuffix(stdout, "\n")
return stdout, err
} }
// Start (activate) a given unit // Start (activate) a given unit
func Start(ctx context.Context, unit string, opts Options) error { //
args := []string{"start", "--system", unit} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func Start(ctx context.Context, unit string, opts Options, args ...string) error {
args[1] = "--user" return start(ctx, unit, opts, args...)
}
_, _, _, err := execute(ctx, args)
return err
} }
// Get back the status string which would be returned by running // Get back the status string which would be returned by running
// `systemctl status [unit]`. // `systemctl status [unit]`.
// //
// Generally, it makes more sense to programatically retrieve the properties // Generally, it makes more sense to programmatically retrieve the properties
// using Show, but this command is provided for the sake of completeness // using Show, but this command is provided for the sake of completeness
func Status(ctx context.Context, unit string, opts Options) (string, error) { //
args := []string{"status", "--system", unit} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func Status(ctx context.Context, unit string, opts Options, args ...string) (string, error) {
args[1] = "--user" return status(ctx, unit, opts, args...)
}
stdout, _, _, err := execute(ctx, args)
return stdout, err
} }
// Stop (deactivate) a given unit // Stop (deactivate) a given unit
func Stop(ctx context.Context, unit string, opts Options) error { //
args := []string{"stop", "--system", unit} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func Stop(ctx context.Context, unit string, opts Options, args ...string) error {
args[1] = "--user" return stop(ctx, unit, opts, args...)
}
_, _, _, err := execute(ctx, args)
return err
} }
// Unmask one or more unit files, as specified on the command line. // Unmask one or more unit files, as specified on the command line.
@@ -236,11 +144,8 @@ func Stop(ctx context.Context, unit string, opts Options) error {
// doesn't exist, but only if it's not already masked. // doesn't exist, but only if it's not already masked.
// If the unit doesn't exist but it's masked anyway, no error will be // If the unit doesn't exist but it's masked anyway, no error will be
// returned. Gross, I know. Take it up with Poettering. // returned. Gross, I know. Take it up with Poettering.
func Unmask(ctx context.Context, unit string, opts Options) error { //
args := []string{"unmask", "--system", unit} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func Unmask(ctx context.Context, unit string, opts Options, args ...string) error {
args[1] = "--user" return unmask(ctx, unit, opts, args...)
}
_, _, _, err := execute(ctx, args)
return err
} }

65
systemctl_darwin.go Normal file
View File

@@ -0,0 +1,65 @@
//go:build !linux
package systemctl
import (
"context"
"github.com/taigrr/systemctl/properties"
)
func daemonReload(_ context.Context, _ Options, _ ...string) error {
return nil
}
func reenable(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}
func disable(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}
func enable(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}
func isActive(_ context.Context, _ string, _ Options, _ ...string) (bool, error) {
return false, nil
}
func isEnabled(_ context.Context, _ string, _ Options, _ ...string) (bool, error) {
return false, nil
}
func isFailed(_ context.Context, _ string, _ Options, _ ...string) (bool, error) {
return false, nil
}
func mask(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}
func restart(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}
func show(_ context.Context, _ string, _ properties.Property, _ Options, _ ...string) (string, error) {
return "", nil
}
func start(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}
func status(_ context.Context, _ string, _ Options, _ ...string) (string, error) {
return "", nil
}
func stop(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}
func unmask(_ context.Context, _ string, _ Options, _ ...string) error {
return nil
}

149
systemctl_linux.go Normal file
View File

@@ -0,0 +1,149 @@
//go:build linux
package systemctl
import (
"context"
"strings"
"github.com/taigrr/systemctl/properties"
)
func daemonReload(ctx context.Context, opts Options, args ...string) error {
a := prepareArgs("daemon-reload", opts, args...)
_, _, _, err := execute(ctx, a)
return err
}
func reenable(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("reenable", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}
func disable(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("disable", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}
func enable(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("enable", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}
func isActive(ctx context.Context, unit string, opts Options, args ...string) (bool, error) {
a := prepareArgs("is-active", opts, append([]string{unit}, args...)...)
stdout, _, _, err := execute(ctx, a)
stdout = strings.TrimSuffix(stdout, "\n")
switch stdout {
case "inactive":
return false, nil
case "active":
return true, nil
case "failed":
return false, nil
case "activating":
return false, nil
default:
return false, err
}
}
func isEnabled(ctx context.Context, unit string, opts Options, args ...string) (bool, error) {
a := prepareArgs("is-enabled", opts, append([]string{unit}, args...)...)
stdout, _, _, err := execute(ctx, a)
stdout = strings.TrimSuffix(stdout, "\n")
switch stdout {
case "enabled":
return true, nil
case "enabled-runtime":
return true, nil
case "linked":
return false, ErrLinked
case "linked-runtime":
return false, ErrLinked
case "alias":
return true, nil
case "masked":
return false, ErrMasked
case "masked-runtime":
return false, ErrMasked
case "static":
return true, nil
case "indirect":
return true, nil
case "disabled":
return false, nil
case "generated":
return true, nil
case "transient":
return true, nil
}
if err != nil {
return false, err
}
return false, ErrUnspecified
}
func isFailed(ctx context.Context, unit string, opts Options, args ...string) (bool, error) {
a := prepareArgs("is-failed", opts, append([]string{unit}, args...)...)
stdout, _, _, err := execute(ctx, a)
stdout = strings.TrimSuffix(stdout, "\n")
switch stdout {
case "inactive":
return false, nil
case "active":
return false, nil
case "failed":
return true, nil
default:
return false, err
}
}
func mask(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("mask", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}
func restart(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("restart", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}
func show(ctx context.Context, unit string, property properties.Property, opts Options, args ...string) (string, error) {
extra := append([]string{unit, "--property", string(property)}, args...)
a := prepareArgs("show", opts, extra...)
stdout, _, _, err := execute(ctx, a)
stdout = strings.TrimPrefix(stdout, string(property)+"=")
stdout = strings.TrimSuffix(stdout, "\n")
return stdout, err
}
func start(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("start", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}
func status(ctx context.Context, unit string, opts Options, args ...string) (string, error) {
a := prepareArgs("status", opts, append([]string{unit}, args...)...)
stdout, _, _, err := execute(ctx, a)
return stdout, err
}
func stop(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("stop", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}
func unmask(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("unmask", opts, append([]string{unit}, args...)...)
_, _, _, err := execute(ctx, a)
return err
}

View File

@@ -2,6 +2,7 @@ package systemctl
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"os/user" "os/user"
@@ -41,7 +42,6 @@ func TestMain(m *testing.M) {
func TestDaemonReload(t *testing.T) { func TestDaemonReload(t *testing.T) {
testCases := []struct { testCases := []struct {
unit string
err error err error
opts Options opts Options
runAsUser bool runAsUser bool
@@ -49,22 +49,26 @@ func TestDaemonReload(t *testing.T) {
/* Run these tests only as a user */ /* Run these tests only as a user */
// fail to reload system daemon as user // fail to reload system daemon as user
{"", ErrInsufficientPermissions, Options{UserMode: false}, true}, {ErrInsufficientPermissions, Options{UserMode: false}, true},
// reload user's scope daemon // reload user's scope daemon
{"", nil, Options{UserMode: true}, true}, {nil, Options{UserMode: true}, true},
/* End user tests*/ /* End user tests*/
/* Run these tests only as a superuser */ /* Run these tests only as a superuser */
// succeed to reload daemon // succeed to reload daemon
{"", nil, Options{UserMode: false}, false}, {nil, Options{UserMode: false}, false},
// fail to connect to user bus as system // fail to connect to user bus as system
{"", ErrBusFailure, Options{UserMode: true}, false}, {ErrBusFailure, Options{UserMode: true}, false},
/* End superuser tests*/ /* End superuser tests*/
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) { mode := "user"
if tc.opts.UserMode == false {
mode = "system"
}
t.Run(fmt.Sprintf("DaemonReload as %s, %s mode", userString, mode), func(t *testing.T) {
if (userString == "root" || userString == "system") && tc.runAsUser { if (userString == "root" || userString == "system") && tc.runAsUser {
t.Skip("skipping user test while running as superuser") t.Skip("skipping user test while running as superuser")
} else if (userString != "root" && userString != "system") && !tc.runAsUser { } else if (userString != "root" && userString != "system") && !tc.runAsUser {
@@ -73,7 +77,7 @@ func TestDaemonReload(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
err := DaemonReload(ctx, tc.opts) err := DaemonReload(ctx, tc.opts)
if err != tc.err { if !errors.Is(err, tc.err) {
t.Errorf("error is %v, but should have been %v", err, tc.err) t.Errorf("error is %v, but should have been %v", err, tc.err)
} }
}) })
@@ -81,78 +85,72 @@ func TestDaemonReload(t *testing.T) {
} }
func TestDisable(t *testing.T) { func TestDisable(t *testing.T) {
t.Run(fmt.Sprintf(""), func(t *testing.T) { if userString != "root" && userString != "system" {
if userString != "root" && userString != "system" { t.Skip("skipping superuser test while running as user")
t.Skip("skipping superuser test while running as user") }
} unit := "nginx"
unit := "nginx" ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel()
defer cancel() err := Mask(ctx, unit, Options{UserMode: false})
err := Mask(ctx, unit, Options{UserMode: false}) if err != nil {
if err != nil { Unmask(ctx, unit, Options{UserMode: false})
Unmask(ctx, unit, Options{UserMode: false}) t.Errorf("Unable to mask %s", unit)
t.Errorf("Unable to mask %s", unit) }
} err = Disable(ctx, unit, Options{UserMode: false})
err = Disable(ctx, unit, Options{UserMode: false}) if !errors.Is(err, ErrMasked) {
if err != ErrMasked { Unmask(ctx, unit, Options{UserMode: false})
Unmask(ctx, unit, Options{UserMode: false}) t.Errorf("error is %v, but should have been %v", err, ErrMasked)
t.Errorf("error is %v, but should have been %v", err, ErrMasked) }
} err = Unmask(ctx, unit, Options{UserMode: false})
err = Unmask(ctx, unit, Options{UserMode: false}) if err != nil {
if err != nil { t.Errorf("Unable to unmask %s", unit)
t.Errorf("Unable to unmask %s", unit) }
}
})
} }
func TestReenable(t *testing.T) { func TestReenable(t *testing.T) {
t.Run(fmt.Sprintf(""), func(t *testing.T) { if userString != "root" && userString != "system" {
if userString != "root" && userString != "system" { t.Skip("skipping superuser test while running as user")
t.Skip("skipping superuser test while running as user") }
} unit := "nginx"
unit := "nginx" ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel()
defer cancel() err := Mask(ctx, unit, Options{UserMode: false})
err := Mask(ctx, unit, Options{UserMode: false}) if err != nil {
if err != nil { Unmask(ctx, unit, Options{UserMode: false})
Unmask(ctx, unit, Options{UserMode: false}) t.Errorf("Unable to mask %s", unit)
t.Errorf("Unable to mask %s", unit) }
} err = Reenable(ctx, unit, Options{UserMode: false})
err = Reenable(ctx, unit, Options{UserMode: false}) if !errors.Is(err, ErrMasked) {
if err != ErrMasked { Unmask(ctx, unit, Options{UserMode: false})
Unmask(ctx, unit, Options{UserMode: false}) t.Errorf("error is %v, but should have been %v", err, ErrMasked)
t.Errorf("error is %v, but should have been %v", err, ErrMasked) }
} err = Unmask(ctx, unit, Options{UserMode: false})
err = Unmask(ctx, unit, Options{UserMode: false}) if err != nil {
if err != nil { t.Errorf("Unable to unmask %s", unit)
t.Errorf("Unable to unmask %s", unit) }
}
})
} }
func TestEnable(t *testing.T) { func TestEnable(t *testing.T) {
t.Run(fmt.Sprintf(""), func(t *testing.T) { if userString != "root" && userString != "system" {
if userString != "root" && userString != "system" { t.Skip("skipping superuser test while running as user")
t.Skip("skipping superuser test while running as user") }
} unit := "nginx"
unit := "nginx" ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel()
defer cancel() err := Mask(ctx, unit, Options{UserMode: false})
err := Mask(ctx, unit, Options{UserMode: false}) if err != nil {
if err != nil { Unmask(ctx, unit, Options{UserMode: false})
Unmask(ctx, unit, Options{UserMode: false}) t.Errorf("Unable to mask %s", unit)
t.Errorf("Unable to mask %s", unit) }
} err = Enable(ctx, unit, Options{UserMode: false})
err = Enable(ctx, unit, Options{UserMode: false}) if !errors.Is(err, ErrMasked) {
if err != ErrMasked { Unmask(ctx, unit, Options{UserMode: false})
Unmask(ctx, unit, Options{UserMode: false}) t.Errorf("error is %v, but should have been %v", err, ErrMasked)
t.Errorf("error is %v, but should have been %v", err, ErrMasked) }
} err = Unmask(ctx, unit, Options{UserMode: false})
err = Unmask(ctx, unit, Options{UserMode: false}) if err != nil {
if err != nil { t.Errorf("Unable to unmask %s", unit)
t.Errorf("Unable to unmask %s", unit) }
}
})
} }
func ExampleEnable() { func ExampleEnable() {
@@ -161,16 +159,16 @@ func ExampleEnable() {
defer cancel() defer cancel()
err := Enable(ctx, unit, Options{UserMode: true}) err := Enable(ctx, unit, Options{UserMode: true})
switch err { switch {
case ErrMasked: case errors.Is(err, ErrMasked):
fmt.Printf("%s is masked, unmask it before enabling\n", unit) fmt.Printf("%s is masked, unmask it before enabling\n", unit)
case ErrDoesNotExist: case errors.Is(err, ErrDoesNotExist):
fmt.Printf("%s does not exist\n", unit) fmt.Printf("%s does not exist\n", unit)
case ErrInsufficientPermissions: case errors.Is(err, ErrInsufficientPermissions):
fmt.Printf("permission to enable %s denied\n", unit) fmt.Printf("permission to enable %s denied\n", unit)
case ErrBusFailure: case errors.Is(err, ErrBusFailure):
fmt.Printf("Cannot communicate with the bus\n") fmt.Printf("Cannot communicate with the bus\n")
case nil: case err == nil:
fmt.Printf("%s enabled successfully\n", unit) fmt.Printf("%s enabled successfully\n", unit)
default: default:
fmt.Printf("Error: %v", err) fmt.Printf("Error: %v", err)
@@ -179,7 +177,7 @@ func ExampleEnable() {
func TestIsActive(t *testing.T) { func TestIsActive(t *testing.T) {
unit := "nginx" unit := "nginx"
t.Run(fmt.Sprintf("check active"), func(t *testing.T) { t.Run("check active", func(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping in short mode") t.Skip("skipping in short mode")
} }
@@ -195,10 +193,10 @@ func TestIsActive(t *testing.T) {
time.Sleep(time.Second) time.Sleep(time.Second)
isActive, err := IsActive(ctx, unit, Options{UserMode: false}) isActive, err := IsActive(ctx, unit, Options{UserMode: false})
if !isActive { if !isActive {
t.Errorf("IsActive didn't return true for %s", unit) t.Errorf("IsActive didn't return true for %s: %v", unit, err)
} }
}) })
t.Run(fmt.Sprintf("check masked"), func(t *testing.T) { t.Run("check masked", func(t *testing.T) {
if userString != "root" && userString != "system" { if userString != "root" && userString != "system" {
t.Skip("skipping superuser test while running as user") t.Skip("skipping superuser test while running as user")
} }
@@ -214,7 +212,7 @@ func TestIsActive(t *testing.T) {
} }
Unmask(ctx, unit, Options{UserMode: false}) Unmask(ctx, unit, Options{UserMode: false})
}) })
t.Run(fmt.Sprintf("check masked"), func(t *testing.T) { t.Run("check masked", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
_, err := IsActive(ctx, "nonexistant", Options{UserMode: false}) _, err := IsActive(ctx, "nonexistant", Options{UserMode: false})
@@ -231,7 +229,7 @@ func TestIsEnabled(t *testing.T) {
userMode = true userMode = true
unit = "syncthing" unit = "syncthing"
} }
t.Run(fmt.Sprintf("check enabled"), func(t *testing.T) { t.Run("check enabled", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
err := Enable(ctx, unit, Options{UserMode: userMode}) err := Enable(ctx, unit, Options{UserMode: userMode})
@@ -240,10 +238,10 @@ func TestIsEnabled(t *testing.T) {
} }
isEnabled, err := IsEnabled(ctx, unit, Options{UserMode: userMode}) isEnabled, err := IsEnabled(ctx, unit, Options{UserMode: userMode})
if !isEnabled { if !isEnabled {
t.Errorf("IsEnabled didn't return true for %s", unit) t.Errorf("IsEnabled didn't return true for %s: %v", unit, err)
} }
}) })
t.Run(fmt.Sprintf("check disabled"), func(t *testing.T) { t.Run("check disabled", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
err := Disable(ctx, unit, Options{UserMode: userMode}) err := Disable(ctx, unit, Options{UserMode: userMode})
@@ -259,7 +257,7 @@ func TestIsEnabled(t *testing.T) {
} }
Enable(ctx, unit, Options{UserMode: false}) Enable(ctx, unit, Options{UserMode: false})
}) })
t.Run(fmt.Sprintf("check masked"), func(t *testing.T) { t.Run("check masked", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
err := Mask(ctx, unit, Options{UserMode: userMode}) err := Mask(ctx, unit, Options{UserMode: userMode})
@@ -320,13 +318,13 @@ func TestMask(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
err := Mask(ctx, tc.unit, tc.opts) err := Mask(ctx, tc.unit, tc.opts)
if err != tc.err { if !errors.Is(err, tc.err) {
t.Errorf("error is %v, but should have been %v", err, tc.err) t.Errorf("error is %v, but should have been %v", err, tc.err)
} }
Unmask(ctx, tc.unit, tc.opts) Unmask(ctx, tc.unit, tc.opts)
}) })
} }
t.Run(fmt.Sprintf("test double masking existing"), func(t *testing.T) { t.Run("test double masking existing", func(t *testing.T) {
unit := "nginx" unit := "nginx"
userMode := false userMode := false
if userString != "root" && userString != "system" { if userString != "root" && userString != "system" {
@@ -346,17 +344,15 @@ func TestMask(t *testing.T) {
} }
Unmask(ctx, unit, opts) Unmask(ctx, unit, opts)
}) })
t.Run(fmt.Sprintf("test double masking nonexisting"), func(t *testing.T) { t.Run("test double masking nonexisting", func(t *testing.T) {
unit := "nonexistant" unit := "nonexistant"
userMode := false userMode := userString != "root" && userString != "system"
if userString != "root" && userString != "system" {
userMode = true
}
opts := Options{UserMode: userMode} opts := Options{UserMode: userMode}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
err := Mask(ctx, unit, opts) err := Mask(ctx, unit, opts)
if err != ErrDoesNotExist { if !errors.Is(err, ErrDoesNotExist) {
t.Errorf("error on initial masking is %v, but should have been %v", err, ErrDoesNotExist) t.Errorf("error on initial masking is %v, but should have been %v", err, ErrDoesNotExist)
} }
err = Mask(ctx, unit, opts) err = Mask(ctx, unit, opts)
@@ -388,9 +384,9 @@ func TestRestart(t *testing.T) {
} }
syscall.Kill(pid, syscall.SIGKILL) syscall.Kill(pid, syscall.SIGKILL)
for { for {
running, err := IsActive(ctx, unit, opts) running, errIsActive := IsActive(ctx, unit, opts)
if err != nil { if errIsActive != nil {
t.Errorf("error asserting %s is up: %v", unit, err) t.Errorf("error asserting %s is up: %v", unit, errIsActive)
break break
} else if running { } else if running {
break break
@@ -415,15 +411,17 @@ func TestShow(t *testing.T) {
UserMode: false, UserMode: false,
} }
for _, x := range properties.Properties { for _, x := range properties.Properties {
t.Run(fmt.Sprintf("show property %s", string(x)), func(t *testing.T) { func(x properties.Property) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) t.Run(fmt.Sprintf("show property %s", string(x)), func(t *testing.T) {
defer cancel() ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
t.Parallel() defer cancel()
_, err := Show(ctx, unit, x, opts) t.Parallel()
if err != nil { _, err := Show(ctx, unit, x, opts)
t.Errorf("error is %v, but should have been %v", err, nil) if err != nil {
} t.Errorf("error is %v, but should have been %v", err, nil)
}) }
})
}(x)
} }
} }
@@ -551,13 +549,13 @@ func TestUnmask(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
err := Mask(ctx, tc.unit, tc.opts) err := Mask(ctx, tc.unit, tc.opts)
if err != tc.err { if !errors.Is(err, tc.err) {
t.Errorf("error is %v, but should have been %v", err, tc.err) t.Errorf("error is %v, but should have been %v", err, tc.err)
} }
Unmask(ctx, tc.unit, tc.opts) Unmask(ctx, tc.unit, tc.opts)
}) })
} }
t.Run(fmt.Sprintf("test double unmasking existing"), func(t *testing.T) { t.Run("test double unmasking existing", func(t *testing.T) {
unit := "nginx" unit := "nginx"
userMode := false userMode := false
if userString != "root" && userString != "system" { if userString != "root" && userString != "system" {
@@ -577,12 +575,10 @@ func TestUnmask(t *testing.T) {
} }
Unmask(ctx, unit, opts) Unmask(ctx, unit, opts)
}) })
t.Run(fmt.Sprintf("test double unmasking nonexisting"), func(t *testing.T) { t.Run("test double unmasking nonexisting", func(t *testing.T) {
unit := "nonexistant" unit := "nonexistant"
userMode := false userMode := userString != "root" && userString != "system"
if userString != "root" && userString != "system" {
userMode = true
}
opts := Options{UserMode: userMode} opts := Options{UserMode: userMode}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
@@ -592,7 +588,7 @@ func TestUnmask(t *testing.T) {
t.Errorf("error on initial unmasking is %v, but should have been %v", err, nil) t.Errorf("error on initial unmasking is %v, but should have been %v", err, nil)
} }
err = Unmask(ctx, unit, opts) err = Unmask(ctx, unit, opts)
if err != ErrDoesNotExist { if !errors.Is(err, ErrDoesNotExist) {
t.Errorf("error on second unmasking is %v, but should have been %v", err, ErrDoesNotExist) t.Errorf("error on second unmasking is %v, but should have been %v", err, ErrDoesNotExist)
} }
}) })

82
util.go
View File

@@ -3,23 +3,19 @@ package systemctl
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"log"
"os/exec" "os/exec"
"regexp" "strings"
) )
var systemctl string var systemctl string
// killed is the exit code returned when a process is terminated by SIGINT.
const killed = 130 const killed = 130
func init() { func init() {
path, err := exec.LookPath("systemctl") path, _ := exec.LookPath("systemctl")
if err != nil {
log.Printf("%v", ErrNotInstalled)
systemctl = ""
return
}
systemctl = path systemctl = path
} }
@@ -34,7 +30,7 @@ func execute(ctx context.Context, args []string) (string, string, int, error) {
) )
if systemctl == "" { if systemctl == "" {
panic(ErrNotInstalled) return "", "", 1, ErrNotInstalled
} }
cmd := exec.CommandContext(ctx, systemctl, args...) cmd := exec.CommandContext(ctx, systemctl, args...)
cmd.Stdout = &stdout cmd.Stdout = &stdout
@@ -44,6 +40,10 @@ func execute(ctx context.Context, args []string) (string, string, int, error) {
warnings = stderr.String() warnings = stderr.String()
code = cmd.ProcessState.ExitCode() code = cmd.ProcessState.ExitCode()
if code == killed {
return output, warnings, code, ErrExecTimeout
}
customErr := filterErr(warnings) customErr := filterErr(warnings)
if customErr != nil { if customErr != nil {
err = customErr err = customErr
@@ -55,33 +55,41 @@ func execute(ctx context.Context, args []string) (string, string, int, error) {
return output, warnings, code, err return output, warnings, code, err
} }
func filterErr(stderr string) error { // prepareArgs builds the systemctl command arguments from a base command,
if matched, _ := regexp.MatchString(`does not exist`, stderr); matched { // options, and any additional arguments the caller wants to pass through.
return ErrDoesNotExist func prepareArgs(base string, opts Options, extra ...string) []string {
args := make([]string, 0, 2+len(extra))
args = append(args, base)
if opts.UserMode {
args = append(args, "--user")
} else {
args = append(args, "--system")
}
args = append(args, extra...)
return args
}
func filterErr(stderr string) error {
switch {
case strings.Contains(stderr, `does not exist`):
return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `not found.`):
return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `not loaded.`):
return errors.Join(ErrUnitNotLoaded, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `No such file or directory`):
return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `Interactive authentication required`):
return errors.Join(ErrInsufficientPermissions, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `Access denied`):
return errors.Join(ErrInsufficientPermissions, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `DBUS_SESSION_BUS_ADDRESS`):
return errors.Join(ErrBusFailure, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `is masked`):
return errors.Join(ErrMasked, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `Failed`):
return errors.Join(ErrUnspecified, fmt.Errorf("stderr: %s", stderr))
default:
return nil
} }
if matched, _ := regexp.MatchString(`not found.`, stderr); matched {
return ErrDoesNotExist
}
if matched, _ := regexp.MatchString(`not loaded.`, stderr); matched {
return ErrUnitNotLoaded
}
if matched, _ := regexp.MatchString(`No such file or directory`, stderr); matched {
return ErrDoesNotExist
}
if matched, _ := regexp.MatchString(`Interactive authentication required`, stderr); matched {
return ErrInsufficientPermissions
}
if matched, _ := regexp.MatchString(`Access denied`, stderr); matched {
return ErrInsufficientPermissions
}
if matched, _ := regexp.MatchString(`DBUS_SESSION_BUS_ADDRESS`, stderr); matched {
return ErrBusFailure
}
if matched, _ := regexp.MatchString(`is masked`, stderr); matched {
return ErrMasked
}
if matched, _ := regexp.MatchString(`Failed`, stderr); matched {
return ErrUnspecified
}
return nil
} }

62
util_test.go Normal file
View File

@@ -0,0 +1,62 @@
package systemctl
import (
"reflect"
"testing"
)
func TestPrepareArgs(t *testing.T) {
tests := []struct {
name string
base string
opts Options
extra []string
expected []string
}{
{
name: "system mode no extra",
base: "start",
opts: Options{},
extra: nil,
expected: []string{"start", "--system"},
},
{
name: "user mode no extra",
base: "start",
opts: Options{UserMode: true},
extra: nil,
expected: []string{"start", "--user"},
},
{
name: "system mode with unit",
base: "start",
opts: Options{},
extra: []string{"nginx.service"},
expected: []string{"start", "--system", "nginx.service"},
},
{
name: "user mode with unit and extra args",
base: "restart",
opts: Options{UserMode: true},
extra: []string{"foo.service", "--no-block"},
expected: []string{"restart", "--user", "foo.service", "--no-block"},
},
{
name: "daemon-reload no extra",
base: "daemon-reload",
opts: Options{},
extra: nil,
expected: []string{"daemon-reload", "--system"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := prepareArgs(tt.base, tt.opts, tt.extra...)
if !reflect.DeepEqual(got, tt.expected) {
t.Errorf("prepareArgs(%q, %+v, %v) = %v, want %v",
tt.base, tt.opts, tt.extra, got, tt.expected)
}
})
}
}