6 Commits

Author SHA1 Message Date
f55bf3005f feat: add optional variadic args to all commands
Allow callers to pass additional systemctl flags (e.g. --no-block,
--force) via variadic string args on every exported function.

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

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

Closes #2
2026-02-26 15:58:19 +00:00
d38136c0dc refactor: clean up unused code, fix typos, improve docs (#7)
* refactor: clean up unused code, fix typos, improve docs

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

* refactor: use unused constants instead of removing them

- Export unitTypes as UnitTypes and add HasValidUnitSuffix helper
- Use killed const (exit code 130) in execute() to detect SIGINT
- Update go.mod to go 1.26
2026-02-23 00:01:59 -05:00
Dat Boi Diego
14c9f0f70d chore: add "support" for non-linux platforms (#6) 2025-09-20 19:34:22 -04:00
5f1537f8bc Merge pull request #4 from MatthiasKunnen/build-lines
Fix build lines conflict with go toolchain version
2025-05-23 14:48:39 -07:00
Matthias Kunnen
d38d347cc6 Fix build lines conflict with go toolchain version
21fce7918e adds build tags, however,
these specific tags are not supported in the go version set in go.mod (1.12).
See <https://go.dev/doc/go1.17#build-lines> and <https://go.dev/doc/go1.18#go-build-lines>.
2025-05-23 17:29:00 +02:00
14a2ca2acd add placehomer unexported unittypes for later 2025-02-18 00:34:16 -08:00
11 changed files with 395 additions and 173 deletions

View File

@@ -32,7 +32,7 @@ If your system isn't running (or targeting another system running) `systemctl`,
## Helper functionality ## Helper functionality
- [x] Get start time of a service (`ExecMainStartTimestamp`) as a `Time` type - [x] Get start time of a service (`ExecMainStartTimestamp`) as a `Time` type
- [x] Get current memory in bytes (`MemoryCurrent`) an an int - [x] Get current memory in bytes (`MemoryCurrent`) as an int
- [x] Get the PID of the main process (`MainPID`) as an int - [x] Get the PID of the main process (`MainPID`) as an int
- [x] Get the restart count of a unit (`NRestarts`) as an int - [x] Get the restart count of a unit (`NRestarts`) as an int

View File

@@ -1,5 +1,3 @@
//go:build linux
package systemctl package systemctl
import ( import (

View File

@@ -13,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

2
go.mod
View File

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

View File

@@ -1,5 +1,3 @@
//go:build linux
package systemctl package systemctl
import ( import (
@@ -37,7 +35,7 @@ func GetNumRestarts(ctx context.Context, unit string, opts Options) (int, error)
return strconv.Atoi(value) return strconv.Atoi(value)
} }
// Get current memory in bytes (`systemctl show [unit] --property MemoryCurrent`) an an int // Get current memory in bytes (`systemctl show [unit] --property MemoryCurrent`) as an int
func GetMemoryUsage(ctx context.Context, unit string, opts Options) (int, error) { 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 {
@@ -58,6 +56,7 @@ 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) { func GetSocketsForServiceUnit(ctx context.Context, unit string, opts Options) ([]string, error) {
args := []string{"list-sockets", "--all", "--no-legend", "--no-pager"} args := []string{"list-sockets", "--all", "--no-legend", "--no-pager"}
if opts.UserMode { if opts.UserMode {
@@ -83,6 +82,7 @@ func GetSocketsForServiceUnit(ctx context.Context, unit string, opts Options) ([
return sockets, nil return sockets, nil
} }
// GetUnits returns a list of all loaded units and their states.
func GetUnits(ctx context.Context, opts Options) ([]Unit, error) { func GetUnits(ctx context.Context, opts Options) ([]Unit, error) {
args := []string{"list-units", "--all", "--no-legend", "--full", "--no-pager"} args := []string{"list-units", "--all", "--no-legend", "--full", "--no-pager"}
if opts.UserMode { if opts.UserMode {
@@ -111,6 +111,7 @@ func GetUnits(ctx context.Context, opts Options) ([]Unit, error) {
return units, nil return units, nil
} }
// GetMaskedUnits returns a list of all masked unit names.
func GetMaskedUnits(ctx context.Context, opts Options) ([]string, error) { func GetMaskedUnits(ctx context.Context, opts Options) ([]string, error) {
args := []string{"list-unit-files", "--state=masked"} args := []string{"list-unit-files", "--state=masked"}
if opts.UserMode { if opts.UserMode {
@@ -140,7 +141,7 @@ func GetMaskedUnits(ctx context.Context, opts Options) ([]string, error) {
return units, nil return units, nil
} }
// check if systemd is the current init system // IsSystemd checks if systemd is the current init system by reading /proc/1/comm.
func IsSystemd() (bool, error) { func IsSystemd() (bool, error) {
b, err := os.ReadFile("/proc/1/comm") b, err := os.ReadFile("/proc/1/comm")
if err != nil { if err != nil {
@@ -149,7 +150,7 @@ func IsSystemd() (bool, error) {
return strings.TrimSpace(string(b)) == "systemd", nil return strings.TrimSpace(string(b)) == "systemd", nil
} }
// check if a service is masked // IsMasked checks if a unit is masked.
func IsMasked(ctx context.Context, unit string, opts Options) (bool, error) { func IsMasked(ctx context.Context, unit string, opts Options) (bool, error) {
units, err := GetMaskedUnits(ctx, opts) units, err := GetMaskedUnits(ctx, opts)
if err != nil { if err != nil {
@@ -163,8 +164,8 @@ func IsMasked(ctx context.Context, unit string, opts Options) (bool, error) {
return false, nil return false, nil
} }
// check if a service is running // IsRunning checks if a unit's sub-state is "running".
// https://unix.stackexchange.com/a/396633 // See https://unix.stackexchange.com/a/396633 for details.
func IsRunning(ctx context.Context, unit string, opts Options) (bool, error) { func IsRunning(ctx context.Context, unit string, opts Options) (bool, error) {
status, err := Show(ctx, unit, properties.SubState, opts) status, err := Show(ctx, unit, properties.SubState, opts)
return status == "running", err return status == "running", err

View File

@@ -1,7 +1,7 @@
//go:build linux
package systemctl package systemctl
import "strings"
type Options struct { type Options struct {
UserMode bool UserMode bool
} }
@@ -13,3 +13,30 @@ type Unit struct {
Sub string Sub string
Description 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

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

65
systemctl_darwin.go Normal file
View File

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

149
systemctl_linux.go Normal file
View File

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

21
util.go
View File

@@ -1,5 +1,3 @@
//go:build linux
package systemctl package systemctl
import ( import (
@@ -13,6 +11,7 @@ import (
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() {
@@ -41,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
@@ -52,6 +55,20 @@ func execute(ctx context.Context, args []string) (string, string, int, error) {
return output, warnings, code, err return output, warnings, code, err
} }
// prepareArgs builds the systemctl command arguments from a base command,
// options, and any additional arguments the caller wants to pass through.
func prepareArgs(base string, opts Options, extra ...string) []string {
args := make([]string, 0, 2+len(extra))
args = append(args, base)
if opts.UserMode {
args = append(args, "--user")
} else {
args = append(args, "--system")
}
args = append(args, extra...)
return args
}
func filterErr(stderr string) error { func filterErr(stderr string) error {
switch { switch {
case strings.Contains(stderr, `does not exist`): case strings.Contains(stderr, `does not exist`):

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