27 Commits

Author SHA1 Message Date
c966da674a ci: drop Go 1.25 from matrix (go.mod requires 1.26) 2026-03-05 22:38:07 +00:00
1b451ee8b2 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
2026-03-05 22:36:39 +00: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
5da4924315 gofumpt against remaining go files 2022-10-23 23:28:19 -07:00
33828cf7b9 gofumpt against systemctl + add reenable 2022-10-23 23:26:52 -07:00
e44344ee7d define unit in README 2022-03-20 23:44:30 -07:00
c8050d2258 fix whitespace 2022-03-20 23:43:30 -07:00
ae23e6ecb9 Fix lowercase typo 2022-03-20 23:42:35 -07:00
c9dec8a0b7 Update README to fix namespace error 2022-03-20 23:41:14 -07:00
f10c42cec1 update go compiler target version 2021-08-17 10:08:09 -07:00
f1979375cd add sponsorship 2021-07-05 21:08:33 -07:00
4e38d03629 move panic from init to exec 2021-06-06 04:55:07 -07:00
701893ebbd Properly return the zero time for invalid timestamps 2021-05-17 10:59:09 -07:00
19 changed files with 1415 additions and 651 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: taigrr # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

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

@@ -0,0 +1,55 @@
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: Run tests with race detection and coverage
run: go test -race -coverprofile=coverage.out -covermode=atomic ./...
- name: Upload coverage to Codecov
if: matrix.go-version == '1.26'
uses: codecov/codecov-action@v5
with:
files: coverage.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
@@ -18,6 +17,7 @@ In fact, if `systemctl` isn't found in the `PATH`, this library will panic.
- [x] `systemctl daemon-reload` - [x] `systemctl daemon-reload`
- [x] `systemctl disable` - [x] `systemctl disable`
- [x] `systemctl enable` - [x] `systemctl enable`
- [x] `systemctl reenable`
- [x] `systemctl is-active` - [x] `systemctl is-active`
- [x] `systemctl is-enabled` - [x] `systemctl is-enabled`
- [x] `systemctl is-failed` - [x] `systemctl is-failed`
@@ -32,7 +32,7 @@ In fact, if `systemctl` isn't found in the `PATH`, this library will panic.
## Helper functionality ## Helper functionality
- [x] Get start time of a service (`ExecMainStartTimestamp`) as a `Time` type - [x] Get start time of a service (`ExecMainStartTimestamp`) as a `Time` type
- [x] Get current memory in bytes (`MemoryCurrent`) an an int - [x] Get current memory in bytes (`MemoryCurrent`) as an int
- [x] Get the PID of the main process (`MainPID`) as an int - [x] Get the PID of the main process (`MainPID`) as an int
- [x] Get the restart count of a unit (`NRestarts`) as an int - [x] Get the restart count of a unit (`NRestarts`) as an int
@@ -65,10 +65,9 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
// Equivalent to `systemctl enable nginx` with a 10 second timeout // Equivalent to `systemctl enable nginx` with a 10 second timeout
opts := Options{ opts := systemctl.Options{ UserMode: false }
usermode: false, unit := "nginx"
} err := systemctl.Enable(ctx, unit, opts)
err := Enable(ctx, unit, opts)
if err != nil { if err != nil {
log.Fatalf("unable to enable unit %s: %v", "nginx", err) log.Fatalf("unable to enable unit %s: %v", "nginx", err)
} }

View File

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

View File

@@ -2,6 +2,7 @@ package systemctl
import ( 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
@@ -25,7 +26,7 @@ func TestErrorFuncs(t *testing.T) {
}{ }{
/* Run these tests only as an unpriviledged user */ /* Run these tests only as an unpriviledged user */
//try nonexistant unit in user mode as user // try nonexistant unit in user mode as user
{"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true}, {"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true},
// 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},
@@ -53,7 +54,6 @@ func TestErrorFuncs(t *testing.T) {
fName := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() fName := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
fName = strings.TrimPrefix(fName, "github.com/taigrr/") fName = strings.TrimPrefix(fName, "github.com/taigrr/")
t.Run(fmt.Sprintf("Errorcheck %s", fName), func(t *testing.T) { t.Run(fmt.Sprintf("Errorcheck %s", fName), func(t *testing.T) {
for _, tc := range errCases { for _, tc := range errCases {
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) {
if (userString == "root" || userString == "system") && tc.runAsUser { if (userString == "root" || userString == "system") && tc.runAsUser {
@@ -64,7 +64,7 @@ func TestErrorFuncs(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
err := f(ctx, tc.unit, tc.opts) err := f(ctx, tc.unit, tc.opts)
if err != tc.err { if !errors.Is(err, tc.err) {
t.Errorf("error is %v, but should have been %v", err, tc.err) t.Errorf("error is %v, but should have been %v", err, tc.err)
} }
}) })

119
filtererr_test.go Normal file
View File

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

4
go.mod
View File

@@ -1,5 +1,3 @@
module github.com/taigrr/systemctl module github.com/taigrr/systemctl
go 1.16 go 1.26
require golang.org/x/tools v0.1.1 // indirect

33
go.sum
View File

@@ -1,33 +0,0 @@
github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1 h1:wGiQel/hW0NnEkJUk8lbzkX2gFJU6PFxf1v5OlCfuOs=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

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"
@@ -13,13 +16,12 @@ const dateFormat = "Mon 2006-01-02 15:04:05 MST"
// Get start time of a service (`systemctl show [unit] --property ExecMainStartTimestamp`) as a `Time` type // Get start time of a service (`systemctl show [unit] --property ExecMainStartTimestamp`) as a `Time` type
func GetStartTime(ctx context.Context, unit string, opts Options) (time.Time, error) { func GetStartTime(ctx context.Context, unit string, opts Options) (time.Time, error) {
value, err := Show(ctx, unit, properties.ExecMainStartTimestamp, opts) value, err := Show(ctx, unit, properties.ExecMainStartTimestamp, opts)
if err != nil { if err != nil {
return time.Unix(0, 0), err return time.Time{}, err
} }
// ExecMainStartTimestamp returns an empty string if the unit is not running // ExecMainStartTimestamp returns an empty string if the unit is not running
if value == "" { if value == "" {
return time.Unix(0, 0), ErrUnitNotActive return time.Time{}, ErrUnitNotActive
} }
return time.Parse(dateFormat, value) return time.Parse(dateFormat, value)
} }
@@ -30,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 {
@@ -53,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"
@@ -27,7 +28,7 @@ func TestGetStartTime(t *testing.T) {
runAsUser bool runAsUser bool
}{ }{
// 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", ErrUnitNotActive, Options{UserMode: false}, true}, {"nonexistant", ErrUnitNotActive, Options{UserMode: false}, true},
// try existing unit in user mode as user // try existing unit in user mode as user
{"syncthing", ErrUnitNotActive, Options{UserMode: true}, true}, {"syncthing", ErrUnitNotActive, Options{UserMode: true}, true},
@@ -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")
} }
@@ -90,21 +91,22 @@ func TestGetStartTime(t *testing.T) {
t.Errorf("Expected start diff to be positive, but got: %d", int(diff)) t.Errorf("Expected start diff to be positive, but got: %d", int(diff))
} }
}) })
} }
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
@@ -170,19 +174,19 @@ func TestGetNumRestarts(t *testing.T) {
t.Errorf("Expected restart count to differ by one, but difference was: %d", secondRestarts-restarts) t.Errorf("Expected restart count to differ by one, but difference was: %d", secondRestarts-restarts)
} }
}) })
} }
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
{"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
{"syncthing", ErrValueNotSet, Options{UserMode: true}, true}, {"syncthing", ErrValueNotSet, Options{UserMode: true}, true},
@@ -199,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 {
@@ -209,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})
@@ -230,18 +236,68 @@ func TestGetMemoryUsage(t *testing.T) {
t.Errorf("Expected memory usage between nginx and user.slice to differ, but both were: %d", bytes) t.Errorf("Expected memory usage between nginx and user.slice to differ, but both were: %d", bytes)
} }
}) })
} }
func TestGetUnits(t *testing.T) {
type testCase struct {
err error
runAsUser bool
opts Options
}
testCases := []testCase{{
// Run these tests only as a user
runAsUser: true,
opts: Options{UserMode: true},
err: nil,
}}
for _, tc := range testCases {
t.Run(fmt.Sprintf("as %s", userString), func(t *testing.T) {
if (userString == "root" || userString == "system") && tc.runAsUser {
t.Skip("skipping user test while running as superuser")
} else if (userString != "root" && userString != "system") && !tc.runAsUser {
t.Skip("skipping superuser test while running as user")
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
units, err := GetUnits(ctx, tc.opts)
if !errors.Is(err, tc.err) {
t.Errorf("error is %v, but should have been %v", err, tc.err)
}
if len(units) == 0 {
t.Errorf("Expected at least one unit, but got none")
}
unit := units[0]
if unit.Name == "" {
t.Errorf("Expected unit name to be non-empty, but got empty")
}
if unit.Load == "" {
t.Errorf("Expected unit load state to be non-empty, but got empty")
}
if unit.Active == "" {
t.Errorf("Expected unit active state to be non-empty, but got empty")
}
if unit.Sub == "" {
t.Errorf("Expected unit sub state to be non-empty, but got empty")
}
if unit.Description == "" {
t.Errorf("Expected unit description to be non-empty, but got empty")
}
})
}
}
func TestGetPID(t *testing.T) { 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
{"nonexistant", nil, Options{UserMode: false}, true}, {"nonexistant", nil, Options{UserMode: false}, true},
// 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},
@@ -258,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 {
@@ -268,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")
} }
@@ -296,7 +354,5 @@ func TestGetPID(t *testing.T) {
if pid == secondPid { if pid == secondPid {
t.Errorf("Expected pid != secondPid, but both were: %d", pid) t.Errorf("Expected pid != secondPid, but both were: %d", pid)
} }
}) })
} }

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,
} }

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,21 @@ 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 { //
var 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.
//
// This removes all symlinks to the unit files backing the specified units from
// the unit configuration directory, then recreates the symlink to the unit again,
// atomically. Can be used to change the symlink target.
//
// Any additional arguments are passed directly to the systemctl command.
func Reenable(ctx context.Context, unit string, opts Options, args ...string) error {
return reenable(ctx, unit, opts, args...)
} }
// Disables one or more units. // Disables one or more units.
@@ -28,13 +34,10 @@ func DaemonReload(ctx context.Context, opts Options) error {
// This removes all symlinks to the unit files backing the specified units from // 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 { //
var 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.
@@ -43,38 +46,20 @@ func Disable(ctx context.Context, unit string, opts Options) error {
// the indicated unit files. After the symlinks have been created, the system // 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 { //
var 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) { //
var 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).
@@ -86,60 +71,17 @@ func IsActive(ctx context.Context, unit string, opts Options) (bool, error) {
// //
// See https://www.freedesktop.org/software/systemd/man/systemctl.html#is-enabled%20UNIT%E2%80%A6 // 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) { //
var 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) { //
var 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
@@ -148,71 +90,51 @@ func IsFailed(ctx context.Context, unit string, opts Options) (bool, error) {
// Notably, Mask may return ErrDoesNotExist if a unit doesn't exist, but it will // 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 { //
var 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 { //
var args = []string{"restart", "--system", unit} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func Restart(ctx context.Context, unit string, opts Options, args ...string) error {
args[1] = "--user" return restart(ctx, unit, opts, args...)
}
_, _, _, err := execute(ctx, args)
return err
} }
// Show a selected property of a unit. Accepted properties are predefined in the // Show a selected property of a unit. Accepted properties are predefined in the
// properties subpackage to guarantee properties are valid and assist code-completion. // properties subpackage to guarantee properties are valid and assist code-completion.
func Show(ctx context.Context, unit string, property properties.Property, opts Options) (string, error) { //
var 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 { //
var 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) { //
var 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 { //
var 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.
@@ -222,11 +144,8 @@ func Stop(ctx context.Context, unit string, opts Options) error {
// doesn't exist, but only if it's not already masked. // 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 { //
var args = []string{"unmask", "--system", unit} // Any additional arguments are passed directly to the systemctl command.
if opts.UserMode { func Unmask(ctx context.Context, unit string, opts Options, args ...string) error {
args[1] = "--user" return unmask(ctx, unit, opts, args...)
}
_, _, _, err := execute(ctx, args)
return err
} }

65
systemctl_darwin.go Normal file
View File

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

149
systemctl_linux.go Normal file
View File

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

View File

@@ -2,6 +2,7 @@ package systemctl
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"os/user" "os/user"
@@ -38,9 +39,9 @@ func TestMain(m *testing.M) {
} }
os.Exit(retCode) os.Exit(retCode)
} }
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
@@ -48,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 {
@@ -72,14 +77,14 @@ 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)
} }
}) })
} }
} }
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")
} }
@@ -92,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)
} }
@@ -100,12 +105,32 @@ 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 TestEnable(t *testing.T) {
t.Run(fmt.Sprintf(""), func(t *testing.T) { func TestReenable(t *testing.T) {
if userString != "root" && userString != "system" {
t.Skip("skipping superuser test while running as user")
}
unit := "nginx"
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := Mask(ctx, unit, Options{UserMode: false})
if err != nil {
Unmask(ctx, unit, Options{UserMode: false})
t.Errorf("Unable to mask %s", unit)
}
err = Reenable(ctx, unit, Options{UserMode: false})
if !errors.Is(err, ErrMasked) {
Unmask(ctx, unit, Options{UserMode: false})
t.Errorf("error is %v, but should have been %v", err, ErrMasked)
}
err = Unmask(ctx, unit, Options{UserMode: false})
if err != nil {
t.Errorf("Unable to unmask %s", unit)
}
}
func TestEnable(t *testing.T) {
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")
} }
@@ -118,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)
} }
@@ -126,25 +151,24 @@ 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() {
unit := "syncthing" unit := "syncthing"
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: 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)
@@ -153,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")
} }
@@ -169,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")
} }
@@ -188,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})
@@ -196,7 +220,6 @@ func TestIsActive(t *testing.T) {
t.Errorf("error is %v, but should have been %v", err, ErrDoesNotExist) t.Errorf("error is %v, but should have been %v", err, ErrDoesNotExist)
} }
}) })
} }
func TestIsEnabled(t *testing.T) { func TestIsEnabled(t *testing.T) {
@@ -206,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})
@@ -215,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})
@@ -234,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})
@@ -251,7 +274,6 @@ func TestIsEnabled(t *testing.T) {
Unmask(ctx, unit, Options{UserMode: userMode}) Unmask(ctx, unit, Options{UserMode: userMode})
Enable(ctx, unit, Options{UserMode: userMode}) Enable(ctx, unit, Options{UserMode: userMode})
}) })
} }
func TestMask(t *testing.T) { func TestMask(t *testing.T) {
@@ -263,7 +285,7 @@ func TestMask(t *testing.T) {
}{ }{
/* Run these tests only as an unpriviledged user */ /* Run these tests only as an unpriviledged user */
//try nonexistant unit in user mode as user // try nonexistant unit in user mode as user
{"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true}, {"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true},
// 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},
@@ -296,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" {
@@ -321,19 +343,16 @@ func TestMask(t *testing.T) {
t.Errorf("error on second masking is %v, but should have been %v", err, nil) t.Errorf("error on second masking is %v, but should have been %v", err, nil)
} }
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)
@@ -342,7 +361,6 @@ func TestMask(t *testing.T) {
} }
Unmask(ctx, unit, opts) Unmask(ctx, unit, opts)
}) })
} }
func TestRestart(t *testing.T) { func TestRestart(t *testing.T) {
@@ -366,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
@@ -381,7 +399,6 @@ func TestRestart(t *testing.T) {
if restarts+1 != secondRestarts { if restarts+1 != secondRestarts {
t.Errorf("Expected restart count to differ by one, but difference was: %d", secondRestarts-restarts) t.Errorf("Expected restart count to differ by one, but difference was: %d", secondRestarts-restarts)
} }
} }
// Runs through all defined Properties in parallel and checks for error cases // Runs through all defined Properties in parallel and checks for error cases
@@ -394,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()
@@ -403,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)
} }
} }
@@ -439,7 +458,6 @@ func TestStart(t *testing.T) {
break break
} }
} }
} }
func TestStatus(t *testing.T) { func TestStatus(t *testing.T) {
@@ -452,7 +470,6 @@ func TestStatus(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("error: %v", err) t.Errorf("error: %v", err)
} }
} }
func TestStop(t *testing.T) { func TestStop(t *testing.T) {
@@ -488,7 +505,6 @@ func TestStop(t *testing.T) {
break break
} }
} }
} }
func TestUnmask(t *testing.T) { func TestUnmask(t *testing.T) {
@@ -500,7 +516,7 @@ func TestUnmask(t *testing.T) {
}{ }{
/* Run these tests only as an unpriviledged user */ /* Run these tests only as an unpriviledged user */
//try nonexistant unit in user mode as user // try nonexistant unit in user mode as user
{"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true}, {"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true},
// 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},
@@ -533,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" {
@@ -558,14 +574,11 @@ func TestUnmask(t *testing.T) {
t.Errorf("error on second unmasking is %v, but should have been %v", err, nil) t.Errorf("error on second unmasking is %v, but should have been %v", err, nil)
} }
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()
@@ -575,9 +588,8 @@ 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)
} }
}) })
} }

80
util.go
View File

@@ -3,20 +3,19 @@ package systemctl
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"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 {
panic(ErrNotInstalled)
}
systemctl = path systemctl = path
} }
@@ -30,6 +29,9 @@ func execute(ctx context.Context, args []string) (string, string, int, error) {
warnings string warnings string
) )
if systemctl == "" {
return "", "", 1, ErrNotInstalled
}
cmd := exec.CommandContext(ctx, systemctl, args...) cmd := exec.CommandContext(ctx, systemctl, args...)
cmd.Stdout = &stdout cmd.Stdout = &stdout
cmd.Stderr = &stderr cmd.Stderr = &stderr
@@ -38,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
@@ -49,33 +55,41 @@ func execute(ctx context.Context, args []string) (string, string, int, error) {
return output, warnings, code, err return output, warnings, code, err
} }
func filterErr(stderr string) error { // prepareArgs builds the systemctl command arguments from a base command,
if matched, _ := regexp.MatchString(`does not exist`, stderr); matched { // options, and any additional arguments the caller wants to pass through.
return ErrDoesNotExist func prepareArgs(base string, opts Options, extra ...string) []string {
args := make([]string, 0, 2+len(extra))
args = append(args, base)
if opts.UserMode {
args = append(args, "--user")
} else {
args = append(args, "--system")
}
args = append(args, extra...)
return args
}
func filterErr(stderr string) error {
switch {
case strings.Contains(stderr, `does not exist`):
return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `not found.`):
return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `not loaded.`):
return errors.Join(ErrUnitNotLoaded, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `No such file or directory`):
return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `Interactive authentication required`):
return errors.Join(ErrInsufficientPermissions, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `Access denied`):
return errors.Join(ErrInsufficientPermissions, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `DBUS_SESSION_BUS_ADDRESS`):
return errors.Join(ErrBusFailure, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `is masked`):
return errors.Join(ErrMasked, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `Failed`):
return errors.Join(ErrUnspecified, fmt.Errorf("stderr: %s", stderr))
default:
return nil
} }
if matched, _ := regexp.MatchString(`not found.`, stderr); matched {
return ErrDoesNotExist
}
if matched, _ := regexp.MatchString(`not loaded.`, stderr); matched {
return ErrUnitNotLoaded
}
if matched, _ := regexp.MatchString(`No such file or directory`, stderr); matched {
return ErrDoesNotExist
}
if matched, _ := regexp.MatchString(`Interactive authentication required`, stderr); matched {
return ErrInsufficientPermissions
}
if matched, _ := regexp.MatchString(`Access denied`, stderr); matched {
return ErrInsufficientPermissions
}
if matched, _ := regexp.MatchString(`DBUS_SESSION_BUS_ADDRESS`, stderr); matched {
return ErrBusFailure
}
if matched, _ := regexp.MatchString(`is masked`, stderr); matched {
return ErrMasked
}
if matched, _ := regexp.MatchString(`Failed`, stderr); matched {
return ErrUnspecified
}
return nil
} }

62
util_test.go Normal file
View File

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