20 Commits

Author SHA1 Message Date
f55bf3005f feat: add optional variadic args to all commands
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 15:58:19 +00: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
5da4924315 gofumpt against remaining go files 2022-10-23 23:28:19 -07:00
33828cf7b9 gofumpt against systemctl + add reenable 2022-10-23 23:26:52 -07:00
e44344ee7d define unit in README 2022-03-20 23:44:30 -07:00
c8050d2258 fix whitespace 2022-03-20 23:43:30 -07:00
ae23e6ecb9 Fix lowercase typo 2022-03-20 23:42:35 -07:00
c9dec8a0b7 Update README to fix namespace error 2022-03-20 23:41:14 -07:00
15 changed files with 1206 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.
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
@@ -18,6 +17,7 @@ In fact, if `systemctl` isn't found in the `PATH`, this library will panic.
- [x] `systemctl daemon-reload`
- [x] `systemctl disable`
- [x] `systemctl enable`
- [x] `systemctl reenable`
- [x] `systemctl is-active`
- [x] `systemctl is-enabled`
- [x] `systemctl is-failed`
@@ -32,7 +32,7 @@ In fact, if `systemctl` isn't found in the `PATH`, this library will panic.
## Helper functionality
- [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 restart count of a unit (`NRestarts`) as an int
@@ -65,10 +65,9 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Equivalent to `systemctl enable nginx` with a 10 second timeout
opts := Options{
usermode: false,
}
err := Enable(ctx, unit, opts)
opts := systemctl.Options{ UserMode: false }
unit := "nginx"
err := systemctl.Enable(ctx, unit, opts)
if err != nil {
log.Fatalf("unable to enable unit %s: %v", "nginx", err)
}

View File

@@ -21,7 +21,6 @@ var (
// Masked units can only be unmasked, but something else was attempted
// Unmask the unit before enabling or disabling it
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
ErrNotInstalled = errors.New("systemctl not in $PATH")
// A unit was expected to be running but was found inactive
@@ -36,5 +35,5 @@ var (
// Something in the stderr output contains the word `Failed`, but it is not a known case
// This is a catch-all, and if it's ever seen in the wild, please submit a PR
ErrUnspecified = errors.New("Unknown error, please submit an issue at github.com/taigrr/systemctl")
ErrUnspecified = errors.New("unknown error, please submit an issue at github.com/taigrr/systemctl")
)

View File

@@ -2,6 +2,7 @@ package systemctl
import (
"context"
"errors"
"fmt"
"reflect"
"runtime"
@@ -12,10 +13,10 @@ import (
func TestErrorFuncs(t *testing.T) {
errFuncs := []func(ctx context.Context, unit string, opts Options) error{
Enable,
Disable,
Restart,
Start,
func(ctx context.Context, unit string, opts Options) error { return Enable(ctx, unit, opts) },
func(ctx context.Context, unit string, opts Options) error { return Disable(ctx, unit, opts) },
func(ctx context.Context, unit string, opts Options) error { return Restart(ctx, unit, opts) },
func(ctx context.Context, unit string, opts Options) error { return Start(ctx, unit, opts) },
}
errCases := []struct {
unit string
@@ -25,7 +26,7 @@ func TestErrorFuncs(t *testing.T) {
}{
/* Run these tests only as an unpriviledged user */
//try nonexistant unit in user mode as user
// try nonexistant unit in user mode as user
{"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true},
// try existing unit in user mode as user
{"syncthing", nil, Options{UserMode: true}, true},
@@ -53,7 +54,6 @@ func TestErrorFuncs(t *testing.T) {
fName := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
fName = strings.TrimPrefix(fName, "github.com/taigrr/")
t.Run(fmt.Sprintf("Errorcheck %s", fName), func(t *testing.T) {
for _, tc := range errCases {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
if (userString == "root" || userString == "system") && tc.runAsUser {
@@ -64,7 +64,7 @@ func TestErrorFuncs(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
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)
}
})

2
go.mod
View File

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

View File

@@ -2,7 +2,10 @@ package systemctl
import (
"context"
"errors"
"os"
"strconv"
"strings"
"time"
"github.com/taigrr/systemctl/properties"
@@ -13,7 +16,6 @@ const dateFormat = "Mon 2006-01-02 15:04:05 MST"
// Get start time of a service (`systemctl show [unit] --property ExecMainStartTimestamp`) as a `Time` type
func GetStartTime(ctx context.Context, unit string, opts Options) (time.Time, error) {
value, err := Show(ctx, unit, properties.ExecMainStartTimestamp, opts)
if err != nil {
return time.Time{}, err
}
@@ -33,7 +35,7 @@ func GetNumRestarts(ctx context.Context, unit string, opts Options) (int, error)
return strconv.Atoi(value)
}
// 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) {
value, err := Show(ctx, unit, properties.MemoryCurrent, opts)
if err != nil {
@@ -53,3 +55,118 @@ func GetPID(ctx context.Context, unit string, opts Options) (int, error) {
}
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 (
"context"
"errors"
"fmt"
"syscall"
"testing"
@@ -27,7 +28,7 @@ func TestGetStartTime(t *testing.T) {
runAsUser bool
}{
// Run these tests only as a user
//try nonexistant unit in user mode as user
// try nonexistant unit in user mode as user
{"nonexistant", ErrUnitNotActive, Options{UserMode: false}, true},
// try existing unit in user mode as user
{"syncthing", ErrUnitNotActive, Options{UserMode: true}, true},
@@ -58,13 +59,13 @@ func TestGetStartTime(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
_, 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)
}
})
}
// 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" {
t.Skip("skipping superuser test while running as user")
}
@@ -90,18 +91,19 @@ func TestGetStartTime(t *testing.T) {
t.Errorf("Expected start diff to be positive, but got: %d", int(diff))
}
})
}
func TestGetNumRestarts(t *testing.T) {
testCases := []struct {
type testCase struct {
unit string
err error
opts Options
runAsUser bool
}{
}
testCases := []testCase{
// 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},
// try existing unit in user mode as user
{"syncthing", ErrValueNotSet, Options{UserMode: true}, true},
@@ -118,23 +120,25 @@ func TestGetNumRestarts(t *testing.T) {
{"nginx", nil, Options{UserMode: false}, false},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
t.Parallel()
if (userString == "root" || userString == "system") && tc.runAsUser {
t.Skip("skipping user test while running as superuser")
} else if (userString != "root" && userString != "system") && !tc.runAsUser {
t.Skip("skipping superuser test while running as user")
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
_, err := GetNumRestarts(ctx, tc.unit, tc.opts)
if err != tc.err {
t.Errorf("error is %v, but should have been %v", err, tc.err)
}
})
func(tc testCase) {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
t.Parallel()
if (userString == "root" || userString == "system") && tc.runAsUser {
t.Skip("skipping user test while running as superuser")
} else if (userString != "root" && userString != "system") && !tc.runAsUser {
t.Skip("skipping superuser test while running as user")
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
_, err := GetNumRestarts(ctx, tc.unit, tc.opts)
if !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
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() {
t.Skip("skipping in short mode")
}
@@ -154,9 +158,9 @@ func TestGetNumRestarts(t *testing.T) {
}
syscall.Kill(pid, syscall.SIGKILL)
for {
running, err := IsActive(ctx, "nginx", Options{UserMode: false})
if err != nil {
t.Errorf("error asserting nginx is up: %v", err)
running, errIsActive := IsActive(ctx, "nginx", Options{UserMode: false})
if errIsActive != nil {
t.Errorf("error asserting nginx is up: %v", errIsActive)
break
} else if running {
break
@@ -170,19 +174,19 @@ func TestGetNumRestarts(t *testing.T) {
t.Errorf("Expected restart count to differ by one, but difference was: %d", secondRestarts-restarts)
}
})
}
func TestGetMemoryUsage(t *testing.T) {
testCases := []struct {
type testCase struct {
unit string
err error
opts Options
runAsUser bool
}{
}
testCases := []testCase{
// 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},
// try existing unit in user mode as user
{"syncthing", ErrValueNotSet, Options{UserMode: true}, true},
@@ -199,23 +203,25 @@ func TestGetMemoryUsage(t *testing.T) {
{"nginx", nil, Options{UserMode: false}, false},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
t.Parallel()
if (userString == "root" || userString == "system") && tc.runAsUser {
t.Skip("skipping user test while running as superuser")
} else if (userString != "root" && userString != "system") && !tc.runAsUser {
t.Skip("skipping superuser test while running as user")
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
_, err := GetMemoryUsage(ctx, tc.unit, tc.opts)
if err != tc.err {
t.Errorf("error is %v, but should have been %v", err, tc.err)
}
})
func(tc testCase) {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
t.Parallel()
if (userString == "root" || userString == "system") && tc.runAsUser {
t.Skip("skipping user test while running as superuser")
} else if (userString != "root" && userString != "system") && !tc.runAsUser {
t.Skip("skipping superuser test while running as user")
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
_, err := GetMemoryUsage(ctx, tc.unit, tc.opts)
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
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)
defer cancel()
bytes, err := GetMemoryUsage(ctx, "nginx", Options{UserMode: false})
@@ -230,18 +236,68 @@ func TestGetMemoryUsage(t *testing.T) {
t.Errorf("Expected memory usage between nginx and user.slice to differ, but both were: %d", bytes)
}
})
}
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) {
testCases := []struct {
type testCase struct {
unit string
err error
opts Options
runAsUser bool
}{
}
testCases := []testCase{
// Run these tests only as a user
//try nonexistant unit in user mode as user
// try nonexistant unit in user mode as user
{"nonexistant", nil, Options{UserMode: false}, true},
// try existing unit in user mode as user
{"syncthing", nil, Options{UserMode: true}, true},
@@ -258,22 +314,24 @@ func TestGetPID(t *testing.T) {
{"nginx", nil, Options{UserMode: false}, false},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
t.Parallel()
if (userString == "root" || userString == "system") && tc.runAsUser {
t.Skip("skipping user test while running as superuser")
} else if (userString != "root" && userString != "system") && !tc.runAsUser {
t.Skip("skipping superuser test while running as user")
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
_, err := GetPID(ctx, tc.unit, tc.opts)
if err != tc.err {
t.Errorf("error is %v, but should have been %v", err, tc.err)
}
})
func(tc testCase) {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
t.Parallel()
if (userString == "root" || userString == "system") && tc.runAsUser {
t.Skip("skipping user test while running as superuser")
} else if (userString != "root" && userString != "system") && !tc.runAsUser {
t.Skip("skipping superuser test while running as user")
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
_, err := GetPID(ctx, tc.unit, tc.opts)
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() {
t.Skip("skipping in short mode")
}
@@ -296,7 +354,5 @@ func TestGetPID(t *testing.T) {
if pid == secondPid {
t.Errorf("Expected pid != secondPid, but both were: %d", pid)
}
})
}

View File

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

View File

@@ -1,5 +1,42 @@
package systemctl
import "strings"
type Options struct {
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 (
"context"
"regexp"
"strings"
"github.com/taigrr/systemctl/properties"
)
@@ -14,13 +12,21 @@ import (
// files, and recreate the entire dependency tree. While the daemon is being
// reloaded, all sockets systemd listens on behalf of user configuration will
// stay accessible.
func DaemonReload(ctx context.Context, opts Options) error {
var args = []string{"daemon-reload", "--system"}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
return err
//
// Any additional arguments are passed directly to the systemctl command.
func DaemonReload(ctx context.Context, opts Options, args ...string) error {
return daemonReload(ctx, opts, args...)
}
// Reenables one or more units.
//
// 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,
// atomically. Can be used to change the symlink target.
//
// Any additional arguments are passed directly to the systemctl command.
func Reenable(ctx context.Context, unit string, opts Options, args ...string) error {
return reenable(ctx, unit, opts, args...)
}
// Disables one or more units.
@@ -28,13 +34,10 @@ func DaemonReload(ctx context.Context, opts Options) error {
// This removes all symlinks to the unit files backing the specified units from
// the unit configuration directory, and hence undoes any changes made by
// enable or link.
func Disable(ctx context.Context, unit string, opts Options) error {
var args = []string{"disable", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
return err
//
// Any additional arguments are passed directly to the systemctl command.
func Disable(ctx context.Context, unit string, opts Options, args ...string) error {
return disable(ctx, unit, opts, args...)
}
// Enable one or more units or unit instances.
@@ -43,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
// manager configuration is reloaded (in a way equivalent to daemon-reload),
// in order to ensure the changes are taken into account immediately.
func Enable(ctx context.Context, unit string, opts Options) error {
var args = []string{"enable", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
return err
//
// Any additional arguments are passed directly to the systemctl command.
func Enable(ctx context.Context, unit string, opts Options, args ...string) error {
return enable(ctx, unit, opts, args...)
}
// Check whether any of the specified units are active (i.e. running).
//
// Returns true if the unit is active, false if inactive or failed.
// Also returns false in an error case.
func IsActive(ctx context.Context, unit string, opts Options) (bool, error) {
var args = []string{"is-active", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
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
}
//
// Any additional arguments are passed directly to the systemctl command.
func IsActive(ctx context.Context, unit string, opts Options, args ...string) (bool, error) {
return isActive(ctx, unit, opts, args...)
}
// Checks whether any of the specified unit files are enabled (as with enable).
@@ -86,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
// for more information
func IsEnabled(ctx context.Context, unit string, opts Options) (bool, error) {
var args = []string{"is-enabled", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
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
//
// Any additional arguments are passed directly to the systemctl command.
func IsEnabled(ctx context.Context, unit string, opts Options, args ...string) (bool, error) {
return isEnabled(ctx, unit, opts, args...)
}
// Check whether any of the specified units are in a "failed" state.
func IsFailed(ctx context.Context, unit string, opts Options) (bool, error) {
var args = []string{"is-failed", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
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
//
// Any additional arguments are passed directly to the systemctl command.
func IsFailed(ctx context.Context, unit string, opts Options, args ...string) (bool, error) {
return isFailed(ctx, unit, opts, args...)
}
// Mask one or more units, as specified on the command line. This will link
@@ -148,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
// continue masking anyway. Calling Mask on a non-existing masked unit does not
// return an error. Similarly, see Unmask.
func Mask(ctx context.Context, unit string, opts Options) error {
var args = []string{"mask", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
return err
//
// Any additional arguments are passed directly to the systemctl command.
func Mask(ctx context.Context, unit string, opts Options, args ...string) error {
return mask(ctx, unit, opts, args...)
}
// Stop and then start one or more units specified on the command line.
// If the units are not running yet, they will be started.
func Restart(ctx context.Context, unit string, opts Options) error {
var args = []string{"restart", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
return err
//
// Any additional arguments are passed directly to the systemctl command.
func Restart(ctx context.Context, unit string, opts Options, args ...string) error {
return restart(ctx, unit, opts, args...)
}
// Show a selected property of a unit. Accepted properties are predefined in the
// 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) {
var args = []string{"show", "--system", unit, "--property", string(property)}
if opts.UserMode {
args[1] = "--user"
}
stdout, _, _, err := execute(ctx, args)
stdout = strings.TrimPrefix(stdout, string(property)+"=")
stdout = strings.TrimSuffix(stdout, "\n")
return stdout, err
//
// Any additional arguments are passed directly to the systemctl command.
func Show(ctx context.Context, unit string, property properties.Property, opts Options, args ...string) (string, error) {
return show(ctx, unit, property, opts, args...)
}
// Start (activate) a given unit
func Start(ctx context.Context, unit string, opts Options) error {
var args = []string{"start", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
return err
//
// Any additional arguments are passed directly to the systemctl command.
func Start(ctx context.Context, unit string, opts Options, args ...string) error {
return start(ctx, unit, opts, args...)
}
// Get back the status string which would be returned by running
// `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
func Status(ctx context.Context, unit string, opts Options) (string, error) {
var args = []string{"status", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
stdout, _, _, err := execute(ctx, args)
return stdout, err
//
// Any additional arguments are passed directly to the systemctl command.
func Status(ctx context.Context, unit string, opts Options, args ...string) (string, error) {
return status(ctx, unit, opts, args...)
}
// Stop (deactivate) a given unit
func Stop(ctx context.Context, unit string, opts Options) error {
var args = []string{"stop", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
return err
//
// Any additional arguments are passed directly to the systemctl command.
func Stop(ctx context.Context, unit string, opts Options, args ...string) error {
return stop(ctx, unit, opts, args...)
}
// Unmask one or more unit files, as specified on the command line.
@@ -222,11 +144,8 @@ func Stop(ctx context.Context, unit string, opts Options) error {
// 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
// returned. Gross, I know. Take it up with Poettering.
func Unmask(ctx context.Context, unit string, opts Options) error {
var args = []string{"unmask", "--system", unit}
if opts.UserMode {
args[1] = "--user"
}
_, _, _, err := execute(ctx, args)
return err
//
// Any additional arguments are passed directly to the systemctl command.
func Unmask(ctx context.Context, unit string, opts Options, args ...string) error {
return unmask(ctx, unit, opts, args...)
}

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 (
"context"
"errors"
"fmt"
"os"
"os/user"
@@ -38,9 +39,9 @@ func TestMain(m *testing.M) {
}
os.Exit(retCode)
}
func TestDaemonReload(t *testing.T) {
testCases := []struct {
unit string
err error
opts Options
runAsUser bool
@@ -48,22 +49,26 @@ func TestDaemonReload(t *testing.T) {
/* Run these tests only as a user */
// fail to reload system daemon as user
{"", ErrInsufficientPermissions, Options{UserMode: false}, true},
{ErrInsufficientPermissions, Options{UserMode: false}, true},
// reload user's scope daemon
{"", nil, Options{UserMode: true}, true},
{nil, Options{UserMode: true}, true},
/* End user tests*/
/* Run these tests only as a superuser */
// succeed to reload daemon
{"", nil, Options{UserMode: false}, false},
{nil, Options{UserMode: false}, false},
// fail to connect to user bus as system
{"", ErrBusFailure, Options{UserMode: true}, false},
{ErrBusFailure, Options{UserMode: true}, false},
/* End superuser tests*/
}
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 {
t.Skip("skipping user test while running as superuser")
} else if (userString != "root" && userString != "system") && !tc.runAsUser {
@@ -72,79 +77,98 @@ func TestDaemonReload(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
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)
}
})
}
}
func TestDisable(t *testing.T) {
t.Run(fmt.Sprintf(""), func(t *testing.T) {
if userString != "root" && userString != "system" {
t.Skip("skipping superuser test while running as user")
}
unit := "nginx"
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := Mask(ctx, unit, Options{UserMode: false})
if err != nil {
Unmask(ctx, unit, Options{UserMode: false})
t.Errorf("Unable to mask %s", unit)
}
err = Disable(ctx, unit, Options{UserMode: false})
if err != ErrMasked {
Unmask(ctx, unit, Options{UserMode: false})
t.Errorf("error is %v, but should have been %v", err, ErrMasked)
}
err = Unmask(ctx, unit, Options{UserMode: false})
if err != nil {
t.Errorf("Unable to unmask %s", unit)
}
})
if userString != "root" && userString != "system" {
t.Skip("skipping superuser test while running as user")
}
unit := "nginx"
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := Mask(ctx, unit, Options{UserMode: false})
if err != nil {
Unmask(ctx, unit, Options{UserMode: false})
t.Errorf("Unable to mask %s", unit)
}
err = Disable(ctx, unit, Options{UserMode: false})
if !errors.Is(err, ErrMasked) {
Unmask(ctx, unit, Options{UserMode: false})
t.Errorf("error is %v, but should have been %v", err, ErrMasked)
}
err = Unmask(ctx, unit, Options{UserMode: false})
if err != nil {
t.Errorf("Unable to unmask %s", unit)
}
}
func TestReenable(t *testing.T) {
if userString != "root" && userString != "system" {
t.Skip("skipping superuser test while running as user")
}
unit := "nginx"
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := Mask(ctx, unit, Options{UserMode: false})
if err != nil {
Unmask(ctx, unit, Options{UserMode: false})
t.Errorf("Unable to mask %s", unit)
}
err = Reenable(ctx, unit, Options{UserMode: false})
if !errors.Is(err, ErrMasked) {
Unmask(ctx, unit, Options{UserMode: false})
t.Errorf("error is %v, but should have been %v", err, ErrMasked)
}
err = Unmask(ctx, unit, Options{UserMode: false})
if err != nil {
t.Errorf("Unable to unmask %s", unit)
}
}
func TestEnable(t *testing.T) {
t.Run(fmt.Sprintf(""), func(t *testing.T) {
if userString != "root" && userString != "system" {
t.Skip("skipping superuser test while running as user")
}
unit := "nginx"
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := Mask(ctx, unit, Options{UserMode: false})
if err != nil {
Unmask(ctx, unit, Options{UserMode: false})
t.Errorf("Unable to mask %s", unit)
}
err = Enable(ctx, unit, Options{UserMode: false})
if err != ErrMasked {
Unmask(ctx, unit, Options{UserMode: false})
t.Errorf("error is %v, but should have been %v", err, ErrMasked)
}
err = Unmask(ctx, unit, Options{UserMode: false})
if err != nil {
t.Errorf("Unable to unmask %s", unit)
}
})
if userString != "root" && userString != "system" {
t.Skip("skipping superuser test while running as user")
}
unit := "nginx"
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := Mask(ctx, unit, Options{UserMode: false})
if err != nil {
Unmask(ctx, unit, Options{UserMode: false})
t.Errorf("Unable to mask %s", unit)
}
err = Enable(ctx, unit, Options{UserMode: false})
if !errors.Is(err, ErrMasked) {
Unmask(ctx, unit, Options{UserMode: false})
t.Errorf("error is %v, but should have been %v", err, ErrMasked)
}
err = Unmask(ctx, unit, Options{UserMode: false})
if err != nil {
t.Errorf("Unable to unmask %s", unit)
}
}
func ExampleEnable() {
unit := "syncthing"
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := Enable(ctx, unit, Options{UserMode: true})
switch err {
case ErrMasked:
switch {
case errors.Is(err, ErrMasked):
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)
case ErrInsufficientPermissions:
case errors.Is(err, ErrInsufficientPermissions):
fmt.Printf("permission to enable %s denied\n", unit)
case ErrBusFailure:
case errors.Is(err, ErrBusFailure):
fmt.Printf("Cannot communicate with the bus\n")
case nil:
case err == nil:
fmt.Printf("%s enabled successfully\n", unit)
default:
fmt.Printf("Error: %v", err)
@@ -153,7 +177,7 @@ func ExampleEnable() {
func TestIsActive(t *testing.T) {
unit := "nginx"
t.Run(fmt.Sprintf("check active"), func(t *testing.T) {
t.Run("check active", func(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}
@@ -169,10 +193,10 @@ func TestIsActive(t *testing.T) {
time.Sleep(time.Second)
isActive, err := IsActive(ctx, unit, Options{UserMode: false})
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" {
t.Skip("skipping superuser test while running as user")
}
@@ -188,7 +212,7 @@ func TestIsActive(t *testing.T) {
}
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)
defer cancel()
_, err := IsActive(ctx, "nonexistant", Options{UserMode: false})
@@ -196,7 +220,6 @@ func TestIsActive(t *testing.T) {
t.Errorf("error is %v, but should have been %v", err, ErrDoesNotExist)
}
})
}
func TestIsEnabled(t *testing.T) {
@@ -206,7 +229,7 @@ func TestIsEnabled(t *testing.T) {
userMode = true
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)
defer cancel()
err := Enable(ctx, unit, Options{UserMode: userMode})
@@ -215,10 +238,10 @@ func TestIsEnabled(t *testing.T) {
}
isEnabled, err := IsEnabled(ctx, unit, Options{UserMode: userMode})
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)
defer cancel()
err := Disable(ctx, unit, Options{UserMode: userMode})
@@ -234,7 +257,7 @@ func TestIsEnabled(t *testing.T) {
}
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)
defer cancel()
err := Mask(ctx, unit, Options{UserMode: userMode})
@@ -251,7 +274,6 @@ func TestIsEnabled(t *testing.T) {
Unmask(ctx, unit, Options{UserMode: userMode})
Enable(ctx, unit, Options{UserMode: userMode})
})
}
func TestMask(t *testing.T) {
@@ -263,7 +285,7 @@ func TestMask(t *testing.T) {
}{
/* Run these tests only as an unpriviledged user */
//try nonexistant unit in user mode as user
// try nonexistant unit in user mode as user
{"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true},
// try existing unit in user mode as user
{"syncthing", nil, Options{UserMode: true}, true},
@@ -296,13 +318,13 @@ func TestMask(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
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)
}
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"
userMode := false
if userString != "root" && userString != "system" {
@@ -321,19 +343,16 @@ func TestMask(t *testing.T) {
t.Errorf("error on second masking is %v, but should have been %v", err, nil)
}
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"
userMode := false
if userString != "root" && userString != "system" {
userMode = true
}
userMode := userString != "root" && userString != "system"
opts := Options{UserMode: userMode}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
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)
}
err = Mask(ctx, unit, opts)
@@ -342,7 +361,6 @@ func TestMask(t *testing.T) {
}
Unmask(ctx, unit, opts)
})
}
func TestRestart(t *testing.T) {
@@ -366,9 +384,9 @@ func TestRestart(t *testing.T) {
}
syscall.Kill(pid, syscall.SIGKILL)
for {
running, err := IsActive(ctx, unit, opts)
if err != nil {
t.Errorf("error asserting %s is up: %v", unit, err)
running, errIsActive := IsActive(ctx, unit, opts)
if errIsActive != nil {
t.Errorf("error asserting %s is up: %v", unit, errIsActive)
break
} else if running {
break
@@ -381,7 +399,6 @@ func TestRestart(t *testing.T) {
if restarts+1 != secondRestarts {
t.Errorf("Expected restart count to differ by one, but difference was: %d", secondRestarts-restarts)
}
}
// Runs through all defined Properties in parallel and checks for error cases
@@ -394,15 +411,17 @@ func TestShow(t *testing.T) {
UserMode: false,
}
for _, x := range properties.Properties {
t.Run(fmt.Sprintf("show property %s", string(x)), func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
t.Parallel()
_, err := Show(ctx, unit, x, opts)
if err != nil {
t.Errorf("error is %v, but should have been %v", err, nil)
}
})
func(x properties.Property) {
t.Run(fmt.Sprintf("show property %s", string(x)), func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
t.Parallel()
_, err := Show(ctx, unit, x, opts)
if err != nil {
t.Errorf("error is %v, but should have been %v", err, nil)
}
})
}(x)
}
}
@@ -439,7 +458,6 @@ func TestStart(t *testing.T) {
break
}
}
}
func TestStatus(t *testing.T) {
@@ -452,7 +470,6 @@ func TestStatus(t *testing.T) {
if err != nil {
t.Errorf("error: %v", err)
}
}
func TestStop(t *testing.T) {
@@ -488,7 +505,6 @@ func TestStop(t *testing.T) {
break
}
}
}
func TestUnmask(t *testing.T) {
@@ -500,7 +516,7 @@ func TestUnmask(t *testing.T) {
}{
/* Run these tests only as an unpriviledged user */
//try nonexistant unit in user mode as user
// try nonexistant unit in user mode as user
{"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true},
// try existing unit in user mode as user
{"syncthing", nil, Options{UserMode: true}, true},
@@ -533,13 +549,13 @@ func TestUnmask(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
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)
}
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"
userMode := false
if userString != "root" && userString != "system" {
@@ -558,14 +574,11 @@ func TestUnmask(t *testing.T) {
t.Errorf("error on second unmasking is %v, but should have been %v", err, nil)
}
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"
userMode := false
if userString != "root" && userString != "system" {
userMode = true
}
userMode := userString != "root" && userString != "system"
opts := Options{UserMode: userMode}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
@@ -575,9 +588,8 @@ func TestUnmask(t *testing.T) {
t.Errorf("error on initial unmasking is %v, but should have been %v", err, nil)
}
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)
}
})
}

82
util.go
View File

@@ -3,23 +3,19 @@ package systemctl
import (
"bytes"
"context"
"errors"
"fmt"
"log"
"os/exec"
"regexp"
"strings"
)
var systemctl string
// killed is the exit code returned when a process is terminated by SIGINT.
const killed = 130
func init() {
path, err := exec.LookPath("systemctl")
if err != nil {
log.Printf("%v", ErrNotInstalled)
systemctl = ""
return
}
path, _ := exec.LookPath("systemctl")
systemctl = path
}
@@ -34,7 +30,7 @@ func execute(ctx context.Context, args []string) (string, string, int, error) {
)
if systemctl == "" {
panic(ErrNotInstalled)
return "", "", 1, ErrNotInstalled
}
cmd := exec.CommandContext(ctx, systemctl, args...)
cmd.Stdout = &stdout
@@ -44,6 +40,10 @@ func execute(ctx context.Context, args []string) (string, string, int, error) {
warnings = stderr.String()
code = cmd.ProcessState.ExitCode()
if code == killed {
return output, warnings, code, ErrExecTimeout
}
customErr := filterErr(warnings)
if customErr != nil {
err = customErr
@@ -55,33 +55,41 @@ func execute(ctx context.Context, args []string) (string, string, int, error) {
return output, warnings, code, err
}
func filterErr(stderr string) error {
if matched, _ := regexp.MatchString(`does not exist`, stderr); matched {
return ErrDoesNotExist
// prepareArgs builds the systemctl command arguments from a base command,
// options, and any additional arguments the caller wants to pass through.
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)
}
})
}
}