18 Commits

Author SHA1 Message Date
da8db0d3a3 Add unit reload support 2026-03-09 16:43:24 -04:00
038fbe1a17 fix(errors): prioritize permission errors over 'does not exist' warnings (#11)
filterErr checked 'does not exist' before 'Interactive authentication
required', so when systemd printed both (common for mask/unmask on
non-installed units as a non-root user), the wrong error was returned.

Reorder checks so permission, bus, and masked errors take priority over
existence warnings. Add tests covering mixed-stderr scenarios.

Also:
- CI: install and start nginx so user + root tests pass
- CI: run tests as both user and root for full coverage
- Bump Go 1.26 → 1.26.1
2026-03-06 11:38:03 -05:00
7253c912ca ci: add test pipeline with codecov, staticcheck, and race detection (#10)
* ci: add test pipeline with codecov, staticcheck, and race detection

- Build/test matrix: Go 1.25 + 1.26
- Race detection enabled for all tests
- Coverage uploaded to Codecov (latest Go only)
- staticcheck lint step
- Go module caching via setup-go

* ci: drop Go 1.25 from matrix (go.mod requires 1.26)
2026-03-05 17:39:38 -05:00
adf3c36632 fix(helpers): return ErrValueNotSet for nonexistent units in GetNumRestarts (#9)
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 12:26:12 -05: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
18 changed files with 1489 additions and 617 deletions

65
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,65 @@
name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]
permissions:
contents: read
jobs:
test:
name: Test (Go ${{ matrix.go-version }})
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ["1.26"]
steps:
- uses: actions/checkout@v4
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: true
- name: Install and start nginx
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq nginx
sudo systemctl start nginx
sudo systemctl enable nginx
- name: Run tests (user)
run: go test -race -coverprofile=coverage-user.out -covermode=atomic ./...
- name: Run tests (root)
run: sudo go test -race -coverprofile=coverage-root.out -covermode=atomic ./...
- name: Upload coverage to Codecov
if: matrix.go-version == '1.26'
uses: codecov/codecov-action@v5
with:
files: coverage-user.out,coverage-root.out
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26"
cache: true
- name: Run staticcheck
uses: dominikh/staticcheck-action@v1
with:
version: latest
install-go: false

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
@@ -23,6 +22,7 @@ In fact, if `systemctl` isn't found in the `PATH`, this library will panic.
- [x] `systemctl is-enabled` - [x] `systemctl is-enabled`
- [x] `systemctl is-failed` - [x] `systemctl is-failed`
- [x] `systemctl mask` - [x] `systemctl mask`
- [x] `systemctl reload`
- [x] `systemctl restart` - [x] `systemctl restart`
- [x] `systemctl show` - [x] `systemctl show`
- [x] `systemctl start` - [x] `systemctl start`
@@ -33,7 +33,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)
} }
}) })

134
filtererr_test.go Normal file
View File

@@ -0,0 +1,134 @@
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: "does not exist with auth required prioritizes permission error",
stderr: "Unit nginx.service does not exist, proceeding anyway.\nFailed to mask unit: Interactive authentication required.",
want: ErrInsufficientPermissions,
},
{
name: "does not exist with access denied prioritizes permission error",
stderr: "Unit foo.service does not exist, proceeding anyway.\nAccess denied",
want: ErrInsufficientPermissions,
},
{
name: "does not exist with bus failure prioritizes bus error",
stderr: "Unit foo.service does not exist, proceeding anyway.\n$DBUS_SESSION_BUS_ADDRESS not set",
want: ErrBusFailure,
},
{
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.1

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,6 +120,7 @@ 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 {
func(tc testCase) {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) { t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
t.Parallel() t.Parallel()
if (userString == "root" || userString == "system") && tc.runAsUser { if (userString == "root" || userString == "system") && tc.runAsUser {
@@ -128,13 +131,14 @@ func TestGetNumRestarts(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 := GetNumRestarts(ctx, tc.unit, tc.opts) _, err := GetNumRestarts(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)
} }
}) })
}(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,6 +203,7 @@ 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 {
func(tc testCase) {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) { t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
t.Parallel() t.Parallel()
if (userString == "root" || userString == "system") && tc.runAsUser { if (userString == "root" || userString == "system") && tc.runAsUser {
@@ -208,13 +214,14 @@ func TestGetMemoryUsage(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 := GetMemoryUsage(ctx, tc.unit, tc.opts) _, err := GetMemoryUsage(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)
} }
}) })
}(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,6 +314,7 @@ 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 {
func(tc testCase) {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) { t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
t.Parallel() t.Parallel()
if (userString == "root" || userString == "system") && tc.runAsUser { if (userString == "root" || userString == "system") && tc.runAsUser {
@@ -267,12 +325,13 @@ func TestGetPID(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 := GetPID(ctx, tc.unit, tc.opts) _, err := GetPID(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)
} }
}) })
}(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,6 +3,7 @@ package properties
type Property string type Property string
const ( const (
Accept Property = "Accept"
ActiveEnterTimestamp Property = "ActiveEnterTimestamp" ActiveEnterTimestamp Property = "ActiveEnterTimestamp"
ActiveEnterTimestampMonotonic Property = "ActiveEnterTimestampMonotonic" ActiveEnterTimestampMonotonic Property = "ActiveEnterTimestampMonotonic"
ActiveExitTimestampMonotonic Property = "ActiveExitTimestampMonotonic" ActiveExitTimestampMonotonic Property = "ActiveExitTimestampMonotonic"
@@ -12,9 +13,13 @@ const (
AssertResult Property = "AssertResult" AssertResult Property = "AssertResult"
AssertTimestamp Property = "AssertTimestamp" AssertTimestamp Property = "AssertTimestamp"
AssertTimestampMonotonic Property = "AssertTimestampMonotonic" AssertTimestampMonotonic Property = "AssertTimestampMonotonic"
Backlog Property = "Backlog"
Before Property = "Before" Before Property = "Before"
BindIPv6Only Property = "BindIPv6Only"
BindLogSockets Property = "BindLogSockets"
BlockIOAccounting Property = "BlockIOAccounting" BlockIOAccounting Property = "BlockIOAccounting"
BlockIOWeight Property = "BlockIOWeight" BlockIOWeight Property = "BlockIOWeight"
Broadcast Property = "Broadcast"
CPUAccounting Property = "CPUAccounting" CPUAccounting Property = "CPUAccounting"
CPUAffinityFromNUMA Property = "CPUAffinityFromNUMA" CPUAffinityFromNUMA Property = "CPUAffinityFromNUMA"
CPUQuotaPerSecUSec Property = "CPUQuotaPerSecUSec" CPUQuotaPerSecUSec Property = "CPUQuotaPerSecUSec"
@@ -28,6 +33,7 @@ const (
CacheDirectoryMode Property = "CacheDirectoryMode" CacheDirectoryMode Property = "CacheDirectoryMode"
CanFreeze Property = "CanFreeze" CanFreeze Property = "CanFreeze"
CanIsolate Property = "CanIsolate" CanIsolate Property = "CanIsolate"
CanLiveMount Property = "CanLiveMount"
CanReload Property = "CanReload" CanReload Property = "CanReload"
CanStart Property = "CanStart" CanStart Property = "CanStart"
CanStop Property = "CanStop" CanStop Property = "CanStop"
@@ -40,17 +46,26 @@ const (
ConfigurationDirectoryMode Property = "ConfigurationDirectoryMode" ConfigurationDirectoryMode Property = "ConfigurationDirectoryMode"
Conflicts Property = "Conflicts" Conflicts Property = "Conflicts"
ControlGroup Property = "ControlGroup" ControlGroup Property = "ControlGroup"
ControlGroupId Property = "ControlGroupId"
ControlPID Property = "ControlPID" ControlPID Property = "ControlPID"
CoredumpFilter Property = "CoredumpFilter" CoredumpFilter Property = "CoredumpFilter"
CoredumpReceive Property = "CoredumpReceive"
DebugInvocation Property = "DebugInvocation"
DefaultDependencies Property = "DefaultDependencies" DefaultDependencies Property = "DefaultDependencies"
DefaultMemoryLow Property = "DefaultMemoryLow" DefaultMemoryLow Property = "DefaultMemoryLow"
DefaultMemoryMin Property = "DefaultMemoryMin" DefaultMemoryMin Property = "DefaultMemoryMin"
DefaultStartupMemoryLow Property = "DefaultStartupMemoryLow"
DeferAcceptUSec Property = "DeferAcceptUSec"
Delegate Property = "Delegate" Delegate Property = "Delegate"
Description Property = "Description" Description Property = "Description"
DevicePolicy Property = "DevicePolicy" DevicePolicy Property = "DevicePolicy"
DirectoryMode Property = "DirectoryMode"
DynamicUser Property = "DynamicUser" DynamicUser Property = "DynamicUser"
EffectiveCPUs Property = "EffectiveCPUs" EffectiveCPUs Property = "EffectiveCPUs"
EffectiveMemoryHigh Property = "EffectiveMemoryHigh"
EffectiveMemoryMax Property = "EffectiveMemoryMax"
EffectiveMemoryNodes Property = "EffectiveMemoryNodes" EffectiveMemoryNodes Property = "EffectiveMemoryNodes"
EffectiveTasksMax Property = "EffectiveTasksMax"
ExecMainCode Property = "ExecMainCode" ExecMainCode Property = "ExecMainCode"
ExecMainExitTimestampMonotonic Property = "ExecMainExitTimestampMonotonic" ExecMainExitTimestampMonotonic Property = "ExecMainExitTimestampMonotonic"
ExecMainPID Property = "ExecMainPID" ExecMainPID Property = "ExecMainPID"
@@ -61,10 +76,14 @@ const (
ExecReloadEx Property = "ExecReloadEx" ExecReloadEx Property = "ExecReloadEx"
ExecStart Property = "ExecStart" ExecStart Property = "ExecStart"
ExecStartEx Property = "ExecStartEx" ExecStartEx Property = "ExecStartEx"
ExtensionImagePolicy Property = "ExtensionImagePolicy"
FailureAction Property = "FailureAction" FailureAction Property = "FailureAction"
FileDescriptorName Property = "FileDescriptorName"
FileDescriptorStoreMax Property = "FileDescriptorStoreMax" FileDescriptorStoreMax Property = "FileDescriptorStoreMax"
FinalKillSignal Property = "FinalKillSignal" FinalKillSignal Property = "FinalKillSignal"
FlushPending Property = "FlushPending"
FragmentPath Property = "FragmentPath" FragmentPath Property = "FragmentPath"
FreeBind Property = "FreeBind"
FreezerState Property = "FreezerState" FreezerState Property = "FreezerState"
GID Property = "GID" GID Property = "GID"
GuessMainPID Property = "GuessMainPID" GuessMainPID Property = "GuessMainPID"
@@ -81,6 +100,8 @@ const (
IPEgressPackets Property = "IPEgressPackets" IPEgressPackets Property = "IPEgressPackets"
IPIngressBytes Property = "IPIngressBytes" IPIngressBytes Property = "IPIngressBytes"
IPIngressPackets Property = "IPIngressPackets" IPIngressPackets Property = "IPIngressPackets"
IPTOS Property = "IPTOS"
IPTTL Property = "IPTTL"
Id Property = "Id" Id Property = "Id"
IgnoreOnIsolate Property = "IgnoreOnIsolate" IgnoreOnIsolate Property = "IgnoreOnIsolate"
IgnoreSIGPIPE Property = "IgnoreSIGPIPE" IgnoreSIGPIPE Property = "IgnoreSIGPIPE"
@@ -91,6 +112,10 @@ const (
JobRunningTimeoutUSec Property = "JobRunningTimeoutUSec" JobRunningTimeoutUSec Property = "JobRunningTimeoutUSec"
JobTimeoutAction Property = "JobTimeoutAction" JobTimeoutAction Property = "JobTimeoutAction"
JobTimeoutUSec Property = "JobTimeoutUSec" JobTimeoutUSec Property = "JobTimeoutUSec"
KeepAlive Property = "KeepAlive"
KeepAliveIntervalUSec Property = "KeepAliveIntervalUSec"
KeepAliveProbes Property = "KeepAliveProbes"
KeepAliveTimeUSec Property = "KeepAliveTimeUSec"
KeyringMode Property = "KeyringMode" KeyringMode Property = "KeyringMode"
KillMode Property = "KillMode" KillMode Property = "KillMode"
KillSignal Property = "KillSignal" KillSignal Property = "KillSignal"
@@ -126,6 +151,7 @@ const (
LimitSIGPENDINGSoft Property = "LimitSIGPENDINGSoft" LimitSIGPENDINGSoft Property = "LimitSIGPENDINGSoft"
LimitSTACK Property = "LimitSTACK" LimitSTACK Property = "LimitSTACK"
LimitSTACKSoft Property = "LimitSTACKSoft" LimitSTACKSoft Property = "LimitSTACKSoft"
Listen Property = "Listen"
LoadState Property = "LoadState" LoadState Property = "LoadState"
LockPersonality Property = "LockPersonality" LockPersonality Property = "LockPersonality"
LogLevelMax Property = "LogLevelMax" LogLevelMax Property = "LogLevelMax"
@@ -134,42 +160,76 @@ const (
LogsDirectoryMode Property = "LogsDirectoryMode" LogsDirectoryMode Property = "LogsDirectoryMode"
MainPID Property = "MainPID" MainPID Property = "MainPID"
ManagedOOMMemoryPressure Property = "ManagedOOMMemoryPressure" ManagedOOMMemoryPressure Property = "ManagedOOMMemoryPressure"
ManagedOOMMemoryPressureDurationUSec Property = "ManagedOOMMemoryPressureDurationUSec"
ManagedOOMMemoryPressureLimit Property = "ManagedOOMMemoryPressureLimit" ManagedOOMMemoryPressureLimit Property = "ManagedOOMMemoryPressureLimit"
ManagedOOMPreference Property = "ManagedOOMPreference" ManagedOOMPreference Property = "ManagedOOMPreference"
ManagedOOMSwap Property = "ManagedOOMSwap" ManagedOOMSwap Property = "ManagedOOMSwap"
Mark Property = "Mark"
MaxConnections Property = "MaxConnections"
MaxConnectionsPerSource Property = "MaxConnectionsPerSource"
MemoryAccounting Property = "MemoryAccounting" MemoryAccounting Property = "MemoryAccounting"
MemoryAvailable Property = "MemoryAvailable"
MemoryCurrent Property = "MemoryCurrent" MemoryCurrent Property = "MemoryCurrent"
MemoryDenyWriteExecute Property = "MemoryDenyWriteExecute" MemoryDenyWriteExecute Property = "MemoryDenyWriteExecute"
MemoryHigh Property = "MemoryHigh" MemoryHigh Property = "MemoryHigh"
MemoryKSM Property = "MemoryKSM"
MemoryLimit Property = "MemoryLimit" MemoryLimit Property = "MemoryLimit"
MemoryLow Property = "MemoryLow" MemoryLow Property = "MemoryLow"
MemoryMax Property = "MemoryMax" MemoryMax Property = "MemoryMax"
MemoryMin Property = "MemoryMin" MemoryMin Property = "MemoryMin"
MemoryPeak Property = "MemoryPeak"
MemoryPressureThresholdUSec Property = "MemoryPressureThresholdUSec"
MemoryPressureWatch Property = "MemoryPressureWatch"
MemorySwapCurrent Property = "MemorySwapCurrent"
MemorySwapMax Property = "MemorySwapMax" MemorySwapMax Property = "MemorySwapMax"
MemorySwapPeak Property = "MemorySwapPeak"
MemoryZSwapCurrent Property = "MemoryZSwapCurrent"
MemoryZSwapMax Property = "MemoryZSwapMax"
MemoryZSwapWriteback Property = "MemoryZSwapWriteback"
MessageQueueMaxMessages Property = "MessageQueueMaxMessages"
MessageQueueMessageSize Property = "MessageQueueMessageSize"
MountAPIVFS Property = "MountAPIVFS" MountAPIVFS Property = "MountAPIVFS"
MountImagePolicy Property = "MountImagePolicy"
NAccepted Property = "NAccepted"
NConnections Property = "NConnections"
NFileDescriptorStore Property = "NFileDescriptorStore" NFileDescriptorStore Property = "NFileDescriptorStore"
NRefused Property = "NRefused"
NRestarts Property = "NRestarts" NRestarts Property = "NRestarts"
NUMAPolicy Property = "NUMAPolicy" NUMAPolicy Property = "NUMAPolicy"
Names Property = "Names" Names Property = "Names"
NeedDaemonReload Property = "NeedDaemonReload" NeedDaemonReload Property = "NeedDaemonReload"
Nice Property = "Nice" Nice Property = "Nice"
NoDelay Property = "NoDelay"
NoNewPrivileges Property = "NoNewPrivileges" NoNewPrivileges Property = "NoNewPrivileges"
NonBlocking Property = "NonBlocking" NonBlocking Property = "NonBlocking"
NotifyAccess Property = "NotifyAccess" NotifyAccess Property = "NotifyAccess"
OOMPolicy Property = "OOMPolicy" OOMPolicy Property = "OOMPolicy"
OOMScoreAdjust Property = "OOMScoreAdjust" OOMScoreAdjust Property = "OOMScoreAdjust"
OnFailureJobMode Property = "OnFailureJobMode" OnFailureJobMode Property = "OnFailureJobMode"
OnSuccessJobMode Property = "OnSuccessJobMode"
PIDFile Property = "PIDFile" PIDFile Property = "PIDFile"
PassCredentials Property = "PassCredentials"
PassFileDescriptorsToExec Property = "PassFileDescriptorsToExec"
PassPacketInfo Property = "PassPacketInfo"
PassSecurity Property = "PassSecurity"
Perpetual Property = "Perpetual" Perpetual Property = "Perpetual"
PipeSize Property = "PipeSize"
PollLimitBurst Property = "PollLimitBurst"
PollLimitIntervalUSec Property = "PollLimitIntervalUSec"
Priority Property = "Priority"
PrivateDevices Property = "PrivateDevices" PrivateDevices Property = "PrivateDevices"
PrivateIPC Property = "PrivateIPC" PrivateIPC Property = "PrivateIPC"
PrivateMounts Property = "PrivateMounts" PrivateMounts Property = "PrivateMounts"
PrivateNetwork Property = "PrivateNetwork" PrivateNetwork Property = "PrivateNetwork"
PrivatePIDs Property = "PrivatePIDs"
PrivateTmp Property = "PrivateTmp" PrivateTmp Property = "PrivateTmp"
PrivateTmpEx Property = "PrivateTmpEx"
PrivateUsers Property = "PrivateUsers" PrivateUsers Property = "PrivateUsers"
PrivateUsersEx Property = "PrivateUsersEx"
ProcSubset Property = "ProcSubset" ProcSubset Property = "ProcSubset"
ProtectClock Property = "ProtectClock" ProtectClock Property = "ProtectClock"
ProtectControlGroups Property = "ProtectControlGroups" ProtectControlGroups Property = "ProtectControlGroups"
ProtectControlGroupsEx Property = "ProtectControlGroupsEx"
ProtectHome Property = "ProtectHome" ProtectHome Property = "ProtectHome"
ProtectHostname Property = "ProtectHostname" ProtectHostname Property = "ProtectHostname"
ProtectKernelLogs Property = "ProtectKernelLogs" ProtectKernelLogs Property = "ProtectKernelLogs"
@@ -177,12 +237,16 @@ const (
ProtectKernelTunables Property = "ProtectKernelTunables" ProtectKernelTunables Property = "ProtectKernelTunables"
ProtectProc Property = "ProtectProc" ProtectProc Property = "ProtectProc"
ProtectSystem Property = "ProtectSystem" ProtectSystem Property = "ProtectSystem"
ReceiveBuffer Property = "ReceiveBuffer"
RefuseManualStart Property = "RefuseManualStart" RefuseManualStart Property = "RefuseManualStart"
RefuseManualStop Property = "RefuseManualStop" RefuseManualStop Property = "RefuseManualStop"
ReloadResult Property = "ReloadResult" ReloadResult Property = "ReloadResult"
RemainAfterExit Property = "RemainAfterExit" RemainAfterExit Property = "RemainAfterExit"
RemoveIPC Property = "RemoveIPC" RemoveIPC Property = "RemoveIPC"
RemoveOnStop Property = "RemoveOnStop"
RequiredBy Property = "RequiredBy"
Requires Property = "Requires" Requires Property = "Requires"
RequiresMountsFor Property = "RequiresMountsFor"
Restart Property = "Restart" Restart Property = "Restart"
RestartKillSignal Property = "RestartKillSignal" RestartKillSignal Property = "RestartKillSignal"
RestartUSec Property = "RestartUSec" RestartUSec Property = "RestartUSec"
@@ -190,15 +254,22 @@ const (
RestrictRealtime Property = "RestrictRealtime" RestrictRealtime Property = "RestrictRealtime"
RestrictSUIDSGID Property = "RestrictSUIDSGID" RestrictSUIDSGID Property = "RestrictSUIDSGID"
Result Property = "Result" Result Property = "Result"
ReusePort Property = "ReusePort"
RootDirectoryStartOnly Property = "RootDirectoryStartOnly" RootDirectoryStartOnly Property = "RootDirectoryStartOnly"
RootEphemeral Property = "RootEphemeral"
RootImagePolicy Property = "RootImagePolicy"
RuntimeDirectoryMode Property = "RuntimeDirectoryMode" RuntimeDirectoryMode Property = "RuntimeDirectoryMode"
RuntimeDirectoryPreserve Property = "RuntimeDirectoryPreserve" RuntimeDirectoryPreserve Property = "RuntimeDirectoryPreserve"
RuntimeMaxUSec Property = "RuntimeMaxUSec" RuntimeMaxUSec Property = "RuntimeMaxUSec"
SameProcessGroup Property = "SameProcessGroup" SameProcessGroup Property = "SameProcessGroup"
SecureBits Property = "SecureBits" SecureBits Property = "SecureBits"
SendBuffer Property = "SendBuffer"
SendSIGHUP Property = "SendSIGHUP" SendSIGHUP Property = "SendSIGHUP"
SendSIGKILL Property = "SendSIGKILL" SendSIGKILL Property = "SendSIGKILL"
SetLoginEnvironment Property = "SetLoginEnvironment"
Slice Property = "Slice" Slice Property = "Slice"
SocketMode Property = "SocketMode"
SocketProtocol Property = "SocketProtocol"
StandardError Property = "StandardError" StandardError Property = "StandardError"
StandardInput Property = "StandardInput" StandardInput Property = "StandardInput"
StandardOutput Property = "StandardOutput" StandardOutput Property = "StandardOutput"
@@ -209,6 +280,11 @@ const (
StartupCPUShares Property = "StartupCPUShares" StartupCPUShares Property = "StartupCPUShares"
StartupCPUWeight Property = "StartupCPUWeight" StartupCPUWeight Property = "StartupCPUWeight"
StartupIOWeight Property = "StartupIOWeight" StartupIOWeight Property = "StartupIOWeight"
StartupMemoryHigh Property = "StartupMemoryHigh"
StartupMemoryLow Property = "StartupMemoryLow"
StartupMemoryMax Property = "StartupMemoryMax"
StartupMemorySwapMax Property = "StartupMemorySwapMax"
StartupMemoryZSwapMax Property = "StartupMemoryZSwapMax"
StateChangeTimestamp Property = "StateChangeTimestamp" StateChangeTimestamp Property = "StateChangeTimestamp"
StateChangeTimestampMonotonic Property = "StateChangeTimestampMonotonic" StateChangeTimestampMonotonic Property = "StateChangeTimestampMonotonic"
StateDirectoryMode Property = "StateDirectoryMode" StateDirectoryMode Property = "StateDirectoryMode"
@@ -216,6 +292,7 @@ const (
StopWhenUnneeded Property = "StopWhenUnneeded" StopWhenUnneeded Property = "StopWhenUnneeded"
SubState Property = "SubState" SubState Property = "SubState"
SuccessAction Property = "SuccessAction" SuccessAction Property = "SuccessAction"
SurviveFinalKillSignal Property = "SurviveFinalKillSignal"
SyslogFacility Property = "SyslogFacility" SyslogFacility Property = "SyslogFacility"
SyslogLevel Property = "SyslogLevel" SyslogLevel Property = "SyslogLevel"
SyslogLevelPrefix Property = "SyslogLevelPrefix" SyslogLevelPrefix Property = "SyslogLevelPrefix"
@@ -233,8 +310,14 @@ const (
TimeoutStartUSec Property = "TimeoutStartUSec" TimeoutStartUSec Property = "TimeoutStartUSec"
TimeoutStopFailureMode Property = "TimeoutStopFailureMode" TimeoutStopFailureMode Property = "TimeoutStopFailureMode"
TimeoutStopUSec Property = "TimeoutStopUSec" TimeoutStopUSec Property = "TimeoutStopUSec"
TimeoutUSec Property = "TimeoutUSec"
TimerSlackNSec Property = "TimerSlackNSec" TimerSlackNSec Property = "TimerSlackNSec"
Timestamping Property = "Timestamping"
Transient Property = "Transient" Transient Property = "Transient"
Transparent Property = "Transparent"
TriggerLimitBurst Property = "TriggerLimitBurst"
TriggerLimitIntervalUSec Property = "TriggerLimitIntervalUSec"
Triggers Property = "Triggers"
Type Property = "Type" Type Property = "Type"
UID Property = "UID" UID Property = "UID"
UMask Property = "UMask" UMask Property = "UMask"
@@ -245,4 +328,5 @@ const (
WatchdogSignal Property = "WatchdogSignal" WatchdogSignal Property = "WatchdogSignal"
WatchdogTimestampMonotonic Property = "WatchdogTimestampMonotonic" WatchdogTimestampMonotonic Property = "WatchdogTimestampMonotonic"
WatchdogUSec Property = "WatchdogUSec" 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,
} }

67
reload_linux_test.go Normal file
View File

@@ -0,0 +1,67 @@
//go:build linux
package systemctl
import (
"context"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
)
func TestReloadBuildsExpectedCommand(t *testing.T) {
tests := []struct {
name string
opts Options
want []string
}{
{
name: "system mode",
opts: Options{},
want: []string{"reload", "--system", "nginx.service"},
},
{
name: "user mode",
opts: Options{UserMode: true},
want: []string{"reload", "--user", "nginx.service"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
logFile := filepath.Join(tempDir, "args.log")
fakeSystemctl := filepath.Join(tempDir, "systemctl")
script := "#!/bin/sh\nprintf '%s\\n' \"$@\" > '" + logFile + "'\n"
if err := os.WriteFile(fakeSystemctl, []byte(script), 0o755); err != nil {
t.Fatalf("write fake systemctl: %v", err)
}
original := systemctl
systemctl = fakeSystemctl
t.Cleanup(func() {
systemctl = original
})
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := Reload(ctx, "nginx.service", tt.opts); err != nil {
t.Fatalf("Reload returned error: %v", err)
}
gotBytes, err := os.ReadFile(logFile)
if err != nil {
t.Fatalf("read captured args: %v", err)
}
got := strings.Fields(strings.TrimSpace(string(gotBytes)))
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("Reload args = %v, want %v", got, tt.want)
}
})
}
}

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,58 @@ 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 // Reload one or more units if they support reload.
//
// Any additional arguments are passed directly to the systemctl command.
func Reload(ctx context.Context, unit string, opts Options, args ...string) error {
return reload(ctx, unit, opts, args...)
} }
// 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 +151,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
} }

69
systemctl_darwin.go Normal file
View File

@@ -0,0 +1,69 @@
//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 reload(_ 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
}

155
systemctl_linux.go Normal file
View File

@@ -0,0 +1,155 @@
//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 reload(ctx context.Context, unit string, opts Options, args ...string) error {
a := prepareArgs("reload", 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,7 +85,6 @@ 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")
} }
@@ -94,7 +97,7 @@ func TestDisable(t *testing.T) {
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 err != ErrMasked { if !errors.Is(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)
} }
@@ -102,11 +105,9 @@ func TestDisable(t *testing.T) {
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")
} }
@@ -119,7 +120,7 @@ func TestReenable(t *testing.T) {
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 err != ErrMasked { if !errors.Is(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)
} }
@@ -127,11 +128,9 @@ func TestReenable(t *testing.T) {
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")
} }
@@ -144,7 +143,7 @@ func TestEnable(t *testing.T) {
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 err != ErrMasked { if !errors.Is(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)
} }
@@ -152,7 +151,6 @@ func TestEnable(t *testing.T) {
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})
@@ -292,7 +290,7 @@ func TestMask(t *testing.T) {
// try existing unit in user mode as user // try existing unit in user mode as user
{"syncthing", nil, Options{UserMode: true}, true}, {"syncthing", nil, Options{UserMode: true}, true},
// try nonexisting unit in system mode as user // try nonexisting unit in system mode as user
{"nonexistant", ErrDoesNotExist, Options{UserMode: false}, true}, {"nonexistant", ErrInsufficientPermissions, Options{UserMode: false}, true},
// try existing unit in system mode as user // try existing unit in system mode as user
{"nginx", ErrInsufficientPermissions, Options{UserMode: false}, true}, {"nginx", ErrInsufficientPermissions, Options{UserMode: false}, true},
@@ -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,6 +411,7 @@ func TestShow(t *testing.T) {
UserMode: false, UserMode: false,
} }
for _, x := range properties.Properties { for _, x := range properties.Properties {
func(x properties.Property) {
t.Run(fmt.Sprintf("show property %s", string(x)), func(t *testing.T) { t.Run(fmt.Sprintf("show property %s", string(x)), 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()
@@ -424,6 +421,7 @@ func TestShow(t *testing.T) {
t.Errorf("error is %v, but should have been %v", err, nil) t.Errorf("error is %v, but should have been %v", err, nil)
} }
}) })
}(x)
} }
} }
@@ -523,7 +521,7 @@ func TestUnmask(t *testing.T) {
// try existing unit in user mode as user // try existing unit in user mode as user
{"syncthing", nil, Options{UserMode: true}, true}, {"syncthing", nil, Options{UserMode: true}, true},
// try nonexisting unit in system mode as user // try nonexisting unit in system mode as user
{"nonexistant", ErrDoesNotExist, Options{UserMode: false}, true}, {"nonexistant", ErrInsufficientPermissions, Options{UserMode: false}, true},
// try existing unit in system mode as user // try existing unit in system mode as user
{"nginx", ErrInsufficientPermissions, Options{UserMode: false}, true}, {"nginx", ErrInsufficientPermissions, Options{UserMode: false}, true},
@@ -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)
} }
}) })

86
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,47 @@ func execute(ctx context.Context, args []string) (string, string, int, error) {
return output, warnings, code, err return output, warnings, code, err
} }
// 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 { func filterErr(stderr string) error {
if matched, _ := regexp.MatchString(`does not exist`, stderr); matched { // Order matters: check higher-priority errors first.
return ErrDoesNotExist // For example, `systemctl mask nginx` as a non-root user on a system
} // without nginx prints both "does not exist, proceeding anyway" (a
if matched, _ := regexp.MatchString(`not found.`, stderr); matched { // warning) and "Interactive authentication required" (the real error).
return ErrDoesNotExist // Permission and bus errors must be checked before "does not exist" so
} // the actual failure reason is returned.
if matched, _ := regexp.MatchString(`not loaded.`, stderr); matched { switch {
return ErrUnitNotLoaded case strings.Contains(stderr, `Interactive authentication required`):
} return errors.Join(ErrInsufficientPermissions, fmt.Errorf("stderr: %s", stderr))
if matched, _ := regexp.MatchString(`No such file or directory`, stderr); matched { case strings.Contains(stderr, `Access denied`):
return ErrDoesNotExist return errors.Join(ErrInsufficientPermissions, fmt.Errorf("stderr: %s", stderr))
} case strings.Contains(stderr, `DBUS_SESSION_BUS_ADDRESS`):
if matched, _ := regexp.MatchString(`Interactive authentication required`, stderr); matched { return errors.Join(ErrBusFailure, fmt.Errorf("stderr: %s", stderr))
return ErrInsufficientPermissions case strings.Contains(stderr, `is masked`):
} return errors.Join(ErrMasked, fmt.Errorf("stderr: %s", stderr))
if matched, _ := regexp.MatchString(`Access denied`, stderr); matched { case strings.Contains(stderr, `does not exist`):
return ErrInsufficientPermissions return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr))
} case strings.Contains(stderr, `not found.`):
if matched, _ := regexp.MatchString(`DBUS_SESSION_BUS_ADDRESS`, stderr); matched { return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr))
return ErrBusFailure case strings.Contains(stderr, `not loaded.`):
} return errors.Join(ErrUnitNotLoaded, fmt.Errorf("stderr: %s", stderr))
if matched, _ := regexp.MatchString(`is masked`, stderr); matched { case strings.Contains(stderr, `No such file or directory`):
return ErrMasked return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr))
} case strings.Contains(stderr, `Failed`):
if matched, _ := regexp.MatchString(`Failed`, stderr); matched { return errors.Join(ErrUnspecified, fmt.Errorf("stderr: %s", stderr))
return ErrUnspecified default:
}
return nil 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)
}
})
}
}