diff --git a/errors_test.go b/errors_test.go index 72d3e45..2ef75f1 100644 --- a/errors_test.go +++ b/errors_test.go @@ -13,10 +13,10 @@ import ( func TestErrorFuncs(t *testing.T) { errFuncs := []func(ctx context.Context, unit string, opts Options) error{ - Enable, - Disable, - Restart, - Start, + func(ctx context.Context, unit string, opts Options) error { return Enable(ctx, unit, opts) }, + func(ctx context.Context, unit string, opts Options) error { return Disable(ctx, unit, opts) }, + func(ctx context.Context, unit string, opts Options) error { return Restart(ctx, unit, opts) }, + func(ctx context.Context, unit string, opts Options) error { return Start(ctx, unit, opts) }, } errCases := []struct { unit string diff --git a/systemctl.go b/systemctl.go index 1dc5f3d..d53a9f1 100644 --- a/systemctl.go +++ b/systemctl.go @@ -12,8 +12,10 @@ import ( // files, and recreate the entire dependency tree. While the daemon is being // reloaded, all sockets systemd listens on behalf of user configuration will // stay accessible. -func DaemonReload(ctx context.Context, opts Options) error { - return daemonReload(ctx, opts) +// +// Any additional arguments are passed directly to the systemctl command. +func DaemonReload(ctx context.Context, opts Options, args ...string) error { + return daemonReload(ctx, opts, args...) } // Reenables one or more units. @@ -21,8 +23,10 @@ func DaemonReload(ctx context.Context, opts Options) error { // This removes all symlinks to the unit files backing the specified units from // the unit configuration directory, then recreates the symlink to the unit again, // atomically. Can be used to change the symlink target. -func Reenable(ctx context.Context, unit string, opts Options) error { - return reenable(ctx, unit, opts) +// +// 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. @@ -30,8 +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 // the unit configuration directory, and hence undoes any changes made by // enable or link. -func Disable(ctx context.Context, unit string, opts Options) error { - return disable(ctx, unit, opts) +// +// Any additional arguments are passed directly to the systemctl command. +func Disable(ctx context.Context, unit string, opts Options, args ...string) error { + return disable(ctx, unit, opts, args...) } // Enable one or more units or unit instances. @@ -40,17 +46,20 @@ func Disable(ctx context.Context, unit string, opts Options) error { // the indicated unit files. After the symlinks have been created, the system // manager configuration is reloaded (in a way equivalent to daemon-reload), // in order to ensure the changes are taken into account immediately. -func Enable(ctx context.Context, unit string, opts Options) error { - return enable(ctx, unit, opts) +// +// Any additional arguments are passed directly to the systemctl command. +func Enable(ctx context.Context, unit string, opts Options, args ...string) error { + return enable(ctx, unit, opts, args...) } // Check whether any of the specified units are active (i.e. running). // // Returns true if the unit is active, false if inactive or failed. // Also returns false in an error case. -func IsActive(ctx context.Context, unit string, opts Options) (bool, error) { - result, err := isActive(ctx, unit, opts) - return result, err +// +// Any additional arguments are passed directly to the systemctl command. +func IsActive(ctx context.Context, unit string, opts Options, args ...string) (bool, error) { + return isActive(ctx, unit, opts, args...) } // Checks whether any of the specified unit files are enabled (as with enable). @@ -62,15 +71,17 @@ func IsActive(ctx context.Context, unit string, opts Options) (bool, error) { // // See https://www.freedesktop.org/software/systemd/man/systemctl.html#is-enabled%20UNIT%E2%80%A6 // for more information -func IsEnabled(ctx context.Context, unit string, opts Options) (bool, error) { - result, err := isEnabled(ctx, unit, opts) - return result, err +// +// Any additional arguments are passed directly to the systemctl command. +func IsEnabled(ctx context.Context, unit string, opts Options, args ...string) (bool, error) { + return isEnabled(ctx, unit, opts, args...) } // Check whether any of the specified units are in a "failed" state. -func IsFailed(ctx context.Context, unit string, opts Options) (bool, error) { - result, err := isFailed(ctx, unit, opts) - return result, err +// +// Any additional arguments are passed directly to the systemctl command. +func IsFailed(ctx context.Context, unit string, opts Options, args ...string) (bool, error) { + return isFailed(ctx, unit, opts, args...) } // Mask one or more units, as specified on the command line. This will link @@ -79,26 +90,33 @@ func IsFailed(ctx context.Context, unit string, opts Options) (bool, error) { // Notably, Mask may return ErrDoesNotExist if a unit doesn't exist, but it will // continue masking anyway. Calling Mask on a non-existing masked unit does not // return an error. Similarly, see Unmask. -func Mask(ctx context.Context, unit string, opts Options) error { - return mask(ctx, unit, opts) +// +// Any additional arguments are passed directly to the systemctl command. +func Mask(ctx context.Context, unit string, opts Options, args ...string) error { + return mask(ctx, unit, opts, args...) } // Stop and then start one or more units specified on the command line. // If the units are not running yet, they will be started. -func Restart(ctx context.Context, unit string, opts Options) error { - return restart(ctx, unit, opts) +// +// Any additional arguments are passed directly to the systemctl command. +func Restart(ctx context.Context, unit string, opts Options, args ...string) error { + return restart(ctx, unit, opts, args...) } // Show a selected property of a unit. Accepted properties are predefined in the // properties subpackage to guarantee properties are valid and assist code-completion. -func Show(ctx context.Context, unit string, property properties.Property, opts Options) (string, error) { - str, err := show(ctx, unit, property, opts) - return str, err +// +// Any additional arguments are passed directly to the systemctl command. +func Show(ctx context.Context, unit string, property properties.Property, opts Options, args ...string) (string, error) { + return show(ctx, unit, property, opts, args...) } // Start (activate) a given unit -func Start(ctx context.Context, unit string, opts Options) error { - return start(ctx, unit, opts) +// +// Any additional arguments are passed directly to the systemctl command. +func Start(ctx context.Context, unit string, opts Options, args ...string) error { + return start(ctx, unit, opts, args...) } // Get back the status string which would be returned by running @@ -106,14 +124,17 @@ func Start(ctx context.Context, unit string, opts Options) error { // // Generally, it makes more sense to programmatically retrieve the properties // using Show, but this command is provided for the sake of completeness -func Status(ctx context.Context, unit string, opts Options) (string, error) { - stat, err := status(ctx, unit, opts) - return stat, err +// +// Any additional arguments are passed directly to the systemctl command. +func Status(ctx context.Context, unit string, opts Options, args ...string) (string, error) { + return status(ctx, unit, opts, args...) } // Stop (deactivate) a given unit -func Stop(ctx context.Context, unit string, opts Options) error { - return stop(ctx, unit, opts) +// +// Any additional arguments are passed directly to the systemctl command. +func Stop(ctx context.Context, unit string, opts Options, args ...string) error { + return stop(ctx, unit, opts, args...) } // Unmask one or more unit files, as specified on the command line. @@ -123,6 +144,8 @@ func Stop(ctx context.Context, unit string, opts Options) error { // doesn't exist, but only if it's not already masked. // If the unit doesn't exist but it's masked anyway, no error will be // returned. Gross, I know. Take it up with Poettering. -func Unmask(ctx context.Context, unit string, opts Options) error { - return unmask(ctx, unit, opts) +// +// Any additional arguments are passed directly to the systemctl command. +func Unmask(ctx context.Context, unit string, opts Options, args ...string) error { + return unmask(ctx, unit, opts, args...) } diff --git a/systemctl_darwin.go b/systemctl_darwin.go index cee1974..a224851 100644 --- a/systemctl_darwin.go +++ b/systemctl_darwin.go @@ -8,58 +8,58 @@ import ( "github.com/taigrr/systemctl/properties" ) -func daemonReload(ctx context.Context, opts Options) error { +func daemonReload(_ context.Context, _ Options, _ ...string) error { return nil } -func reenable(ctx context.Context, unit string, opts Options) error { +func reenable(_ context.Context, _ string, _ Options, _ ...string) error { return nil } -func disable(ctx context.Context, unit string, opts Options) error { +func disable(_ context.Context, _ string, _ Options, _ ...string) error { return nil } -func enable(ctx context.Context, unit string, opts Options) error { +func enable(_ context.Context, _ string, _ Options, _ ...string) error { return nil } -func isActive(ctx context.Context, unit string, opts Options) (bool, error) { +func isActive(_ context.Context, _ string, _ Options, _ ...string) (bool, error) { return false, nil } -func isEnabled(ctx context.Context, unit string, opts Options) (bool, error) { +func isEnabled(_ context.Context, _ string, _ Options, _ ...string) (bool, error) { return false, nil } -func isFailed(ctx context.Context, unit string, opts Options) (bool, error) { +func isFailed(_ context.Context, _ string, _ Options, _ ...string) (bool, error) { return false, nil } -func mask(ctx context.Context, unit string, opts Options) error { +func mask(_ context.Context, _ string, _ Options, _ ...string) error { return nil } -func restart(ctx context.Context, unit string, opts Options) error { +func restart(_ context.Context, _ string, _ Options, _ ...string) error { return nil } -func show(ctx context.Context, unit string, property properties.Property, opts Options) (string, error) { +func show(_ context.Context, _ string, _ properties.Property, _ Options, _ ...string) (string, error) { return "", nil } -func start(ctx context.Context, unit string, opts Options) error { +func start(_ context.Context, _ string, _ Options, _ ...string) error { return nil } -func status(ctx context.Context, unit string, opts Options) (string, error) { +func status(_ context.Context, _ string, _ Options, _ ...string) (string, error) { return "", nil } -func stop(ctx context.Context, unit string, opts Options) error { +func stop(_ context.Context, _ string, _ Options, _ ...string) error { return nil } -func unmask(ctx context.Context, unit string, opts Options) error { +func unmask(_ context.Context, _ string, _ Options, _ ...string) error { return nil } diff --git a/systemctl_linux.go b/systemctl_linux.go index ad866c0..d40dcc3 100644 --- a/systemctl_linux.go +++ b/systemctl_linux.go @@ -9,74 +9,33 @@ import ( "github.com/taigrr/systemctl/properties" ) -// Reload systemd manager configuration. -// -// This will rerun all generators (see systemd. generator(7)), reload all unit -// files, and recreate the entire dependency tree. While the daemon is being -// reloaded, all sockets systemd listens on behalf of user configuration will -// stay accessible. -func daemonReload(ctx context.Context, opts Options) error { - args := []string{"daemon-reload", "--system"} - if opts.UserMode { - args[1] = "--user" - } - _, _, _, err := execute(ctx, args) +func daemonReload(ctx context.Context, opts Options, args ...string) error { + a := prepareArgs("daemon-reload", opts, args...) + _, _, _, err := execute(ctx, a) 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. -func reenable(ctx context.Context, unit string, opts Options) error { - args := []string{"reenable", "--system", unit} - if opts.UserMode { - args[1] = "--user" - } - _, _, _, err := execute(ctx, args) +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 } -// Disables one or more units. -// -// This removes all symlinks to the unit files backing the specified units from -// the unit configuration directory, and hence undoes any changes made by -// enable or link. -func disable(ctx context.Context, unit string, opts Options) error { - args := []string{"disable", "--system", unit} - if opts.UserMode { - args[1] = "--user" - } - _, _, _, err := execute(ctx, args) +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 } -// Enable one or more units or unit instances. -// -// This will create a set of symlinks, as encoded in the [Install] sections of -// the indicated unit files. After the symlinks have been created, the system -// manager configuration is reloaded (in a way equivalent to daemon-reload), -// in order to ensure the changes are taken into account immediately. -func enable(ctx context.Context, unit string, opts Options) error { - args := []string{"enable", "--system", unit} - if opts.UserMode { - args[1] = "--user" - } - _, _, _, err := execute(ctx, args) +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 } -// Check whether any of the specified units are active (i.e. running). -// -// Returns true if the unit is active, false if inactive or failed. -// Also returns false in an error case. -func isActive(ctx context.Context, unit string, opts Options) (bool, error) { - args := []string{"is-active", "--system", unit} - if opts.UserMode { - args[1] = "--user" - } - stdout, _, _, err := execute(ctx, args) +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": @@ -92,21 +51,9 @@ func isActive(ctx context.Context, unit string, opts Options) (bool, error) { } } -// Checks whether any of the specified unit files are enabled (as with enable). -// -// Returns true if the unit is enabled, aliased, static, indirect, generated -// or transient. -// -// Returns false if disabled. Also returns an error if linked, masked, or bad. -// -// See https://www.freedesktop.org/software/systemd/man/systemctl.html#is-enabled%20UNIT%E2%80%A6 -// for more information -func isEnabled(ctx context.Context, unit string, opts Options) (bool, error) { - args := []string{"is-enabled", "--system", unit} - if opts.UserMode { - args[1] = "--user" - } - stdout, _, _, err := execute(ctx, args) +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": @@ -140,13 +87,9 @@ func isEnabled(ctx context.Context, unit string, opts Options) (bool, error) { return false, ErrUnspecified } -// 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} - if opts.UserMode { - args[1] = "--user" - } - stdout, _, _, err := execute(ctx, args) +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": @@ -160,91 +103,47 @@ func isFailed(ctx context.Context, unit string, opts Options) (bool, error) { } } -// Mask one or more units, as specified on the command line. This will link -// these unit files to /dev/null, making it impossible to start them. -// -// Notably, Mask may return ErrDoesNotExist if a unit doesn't exist, but it will -// continue masking anyway. Calling Mask on a non-existing masked unit does not -// return an error. Similarly, see Unmask. -func mask(ctx context.Context, unit string, opts Options) error { - args := []string{"mask", "--system", unit} - if opts.UserMode { - args[1] = "--user" - } - _, _, _, err := execute(ctx, args) +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 } -// Stop and then start one or more units specified on the command line. -// If the units are not running yet, they will be started. -func restart(ctx context.Context, unit string, opts Options) error { - args := []string{"restart", "--system", unit} - if opts.UserMode { - args[1] = "--user" - } - _, _, _, err := execute(ctx, args) +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 } -// Show a selected property of a unit. Accepted properties are predefined in the -// properties subpackage to guarantee properties are valid and assist code-completion. -func show(ctx context.Context, unit string, property properties.Property, opts Options) (string, error) { - args := []string{"show", "--system", unit, "--property", string(property)} - if opts.UserMode { - args[1] = "--user" - } - stdout, _, _, err := execute(ctx, args) +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 } -// Start (activate) a given unit -func start(ctx context.Context, unit string, opts Options) error { - args := []string{"start", "--system", unit} - if opts.UserMode { - args[1] = "--user" - } - _, _, _, err := execute(ctx, args) +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 } -// Get back the status string which would be returned by running -// `systemctl status [unit]`. -// -// Generally, it makes more sense to programmatically retrieve the properties -// using Show, but this command is provided for the sake of completeness -func status(ctx context.Context, unit string, opts Options) (string, error) { - args := []string{"status", "--system", unit} - if opts.UserMode { - args[1] = "--user" - } - stdout, _, _, err := execute(ctx, args) +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 } -// Stop (deactivate) a given unit -func stop(ctx context.Context, unit string, opts Options) error { - args := []string{"stop", "--system", unit} - if opts.UserMode { - args[1] = "--user" - } - _, _, _, err := execute(ctx, args) +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 } -// Unmask one or more unit files, as specified on the command line. -// This will undo the effect of Mask. -// -// In line with systemd, Unmask will return ErrDoesNotExist if the unit -// doesn't exist, but only if it's not already masked. -// If the unit doesn't exist but it's masked anyway, no error will be -// returned. Gross, I know. Take it up with Poettering. -func unmask(ctx context.Context, unit string, opts Options) error { - args := []string{"unmask", "--system", unit} - if opts.UserMode { - args[1] = "--user" - } - _, _, _, err := execute(ctx, args) +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 } diff --git a/util.go b/util.go index 75f4d7a..84257c1 100644 --- a/util.go +++ b/util.go @@ -55,6 +55,20 @@ func execute(ctx context.Context, args []string) (string, string, int, error) { 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 { switch { case strings.Contains(stderr, `does not exist`): diff --git a/util_test.go b/util_test.go new file mode 100644 index 0000000..79545d3 --- /dev/null +++ b/util_test.go @@ -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) + } + }) + } +}