├── .github └── FUNDING.yml ├── LICENSE ├── README.md ├── errors.go ├── errors_test.go ├── go.mod ├── go.sum ├── helpers.go ├── helpers_test.go ├── properties ├── properties.go └── properties_list.go ├── structs.go ├── systemctl.go ├── systemctl_test.go └── util.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: taigrr # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021 by Tai Groot 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 7 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 8 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 9 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 10 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 11 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 12 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/taigrr/systemctl)](https://pkg.go.dev/github.com/taigrr/systemctl) 2 | # systemctl 3 | 4 | This library aims at providing idiomatic `systemctl` bindings for go developers, in order to make it easier to write system tooling using golang. 5 | 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. 6 | 7 | If your system isn't running (or targeting another system running) `systemctl`, this library will be of little use to you. 8 | 9 | ## What is systemctl 10 | 11 | `systemctl` is a command-line program which grants the user control over the systemd system and service manager. 12 | 13 | `systemctl` may be used to introspect and control the state of the "systemd" system and service manager. Please refer to `systemd(1)` for an introduction into the basic concepts and functionality this tool manages. 14 | 15 | ## Supported systemctl functions 16 | 17 | - [x] `systemctl daemon-reload` 18 | - [x] `systemctl disable` 19 | - [x] `systemctl enable` 20 | - [x] `systemctl reenable` 21 | - [x] `systemctl is-active` 22 | - [x] `systemctl is-enabled` 23 | - [x] `systemctl is-failed` 24 | - [x] `systemctl mask` 25 | - [x] `systemctl restart` 26 | - [x] `systemctl show` 27 | - [x] `systemctl start` 28 | - [x] `systemctl status` 29 | - [x] `systemctl stop` 30 | - [x] `systemctl unmask` 31 | 32 | ## Helper functionality 33 | 34 | - [x] Get start time of a service (`ExecMainStartTimestamp`) as a `Time` type 35 | - [x] Get current memory in bytes (`MemoryCurrent`) an an int 36 | - [x] Get the PID of the main process (`MainPID`) as an int 37 | - [x] Get the restart count of a unit (`NRestarts`) as an int 38 | 39 | 40 | ## Useful errors 41 | 42 | All functions return a predefined error type, and it is highly recommended these errors are handled properly. 43 | 44 | ## Context support 45 | 46 | All calls into this library support go's `context` functionality. 47 | Therefore, blocking calls can time out according to the caller's needs, and the returned error should be checked to see if a timeout occurred (`ErrExecTimeout`). 48 | 49 | 50 | ## Simple example 51 | 52 | ```go 53 | package main 54 | 55 | import ( 56 | "context" 57 | "fmt" 58 | "log" 59 | "time" 60 | 61 | "github.com/taigrr/systemctl" 62 | ) 63 | 64 | func main() { 65 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 66 | defer cancel() 67 | // Equivalent to `systemctl enable nginx` with a 10 second timeout 68 | opts := systemctl.Options{ UserMode: false } 69 | unit := "nginx" 70 | err := systemctl.Enable(ctx, unit, opts) 71 | if err != nil { 72 | log.Fatalf("unable to enable unit %s: %v", "nginx", err) 73 | } 74 | } 75 | ``` 76 | 77 | ## License 78 | 79 | This project is licensed under the 0BSD License, written by [Rob Landley](https://github.com/landley). 80 | As such, you may use this library without restriction or attribution, but please don't pass it off as your own. 81 | Attribution, though not required, is appreciated. 82 | 83 | By contributing, you agree all code submitted also falls under the License. 84 | 85 | ## External resources 86 | 87 | - [Official systemctl documentation](https://www.man7.org/linux/man-pages/man1/systemctl.1.html) 88 | - [Inspiration for this repo](https://github.com/Ullaakut/nmap/) 89 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package systemctl 4 | 5 | import ( 6 | "errors" 7 | ) 8 | 9 | var ( 10 | // $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR were not defined 11 | // This usually is the result of running in usermode as root 12 | ErrBusFailure = errors.New("bus connection failure") 13 | // The unit specified doesn't exist or can't be found 14 | ErrDoesNotExist = errors.New("unit does not exist") 15 | // The provided context was cancelled before the command finished execution 16 | ErrExecTimeout = errors.New("command timed out") 17 | // The executable was invoked without enough permissions to run the selected command 18 | // Running as superuser or adding the correct PolicyKit definitions can fix this 19 | // See https://wiki.debian.org/PolicyKit for more information 20 | ErrInsufficientPermissions = errors.New("insufficient permissions") 21 | // Selected unit file resides outside of the unit file search path 22 | ErrLinked = errors.New("unit file linked") 23 | // Masked units can only be unmasked, but something else was attempted 24 | // Unmask the unit before enabling or disabling it 25 | ErrMasked = errors.New("unit masked") 26 | // Make sure systemctl is in the PATH before calling again 27 | ErrNotInstalled = errors.New("systemctl not in $PATH") 28 | // A unit was expected to be running but was found inactive 29 | // This can happen when calling GetStartTime on a dead unit, for example 30 | ErrUnitNotActive = errors.New("unit not active") 31 | // A unit was expected to be loaded, but was not. 32 | // This can happen when trying to Stop a unit which does not exist, for example 33 | ErrUnitNotLoaded = errors.New("unit not loaded") 34 | // An expected value is unavailable, but the unit may be running 35 | // This can happen when calling GetMemoryUsage on systemd itself, for example 36 | ErrValueNotSet = errors.New("value not set") 37 | 38 | // Something in the stderr output contains the word `Failed`, but it is not a known case 39 | // This is a catch-all, and if it's ever seen in the wild, please submit a PR 40 | ErrUnspecified = errors.New("unknown error, please submit an issue at github.com/taigrr/systemctl") 41 | ) 42 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package systemctl 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | "runtime" 9 | "strings" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestErrorFuncs(t *testing.T) { 15 | errFuncs := []func(ctx context.Context, unit string, opts Options) error{ 16 | Enable, 17 | Disable, 18 | Restart, 19 | Start, 20 | } 21 | errCases := []struct { 22 | unit string 23 | err error 24 | opts Options 25 | runAsUser bool 26 | }{ 27 | /* Run these tests only as an unpriviledged user */ 28 | 29 | // try nonexistant unit in user mode as user 30 | {"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true}, 31 | // try existing unit in user mode as user 32 | {"syncthing", nil, Options{UserMode: true}, true}, 33 | // try nonexisting unit in system mode as user 34 | {"nonexistant", ErrInsufficientPermissions, Options{UserMode: false}, true}, 35 | // try existing unit in system mode as user 36 | {"nginx", ErrInsufficientPermissions, Options{UserMode: false}, true}, 37 | 38 | /* End user tests*/ 39 | 40 | /* Run these tests only as a superuser */ 41 | 42 | // try nonexistant unit in system mode as system 43 | {"nonexistant", ErrDoesNotExist, Options{UserMode: false}, false}, 44 | // try existing unit in system mode as system 45 | {"nginx", ErrBusFailure, Options{UserMode: true}, false}, 46 | // try existing unit in system mode as system 47 | {"nginx", nil, Options{UserMode: false}, false}, 48 | 49 | /* End superuser tests*/ 50 | 51 | } 52 | 53 | for _, f := range errFuncs { 54 | fName := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() 55 | fName = strings.TrimPrefix(fName, "github.com/taigrr/") 56 | t.Run(fmt.Sprintf("Errorcheck %s", fName), func(t *testing.T) { 57 | for _, tc := range errCases { 58 | t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) { 59 | if (userString == "root" || userString == "system") && tc.runAsUser { 60 | t.Skip("skipping user test while running as superuser") 61 | } else if (userString != "root" && userString != "system") && !tc.runAsUser { 62 | t.Skip("skipping superuser test while running as user") 63 | } 64 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 65 | defer cancel() 66 | err := f(ctx, tc.unit, tc.opts) 67 | if !errors.Is(err, tc.err) { 68 | t.Errorf("error is %v, but should have been %v", err, tc.err) 69 | } 70 | }) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/taigrr/systemctl 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taigrr/systemctl/5f1537f8bc784db551b6d0c030f2d7654cb3f66b/go.sum -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package systemctl 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "os" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/taigrr/systemctl/properties" 14 | ) 15 | 16 | const dateFormat = "Mon 2006-01-02 15:04:05 MST" 17 | 18 | // Get start time of a service (`systemctl show [unit] --property ExecMainStartTimestamp`) as a `Time` type 19 | func GetStartTime(ctx context.Context, unit string, opts Options) (time.Time, error) { 20 | value, err := Show(ctx, unit, properties.ExecMainStartTimestamp, opts) 21 | if err != nil { 22 | return time.Time{}, err 23 | } 24 | // ExecMainStartTimestamp returns an empty string if the unit is not running 25 | if value == "" { 26 | return time.Time{}, ErrUnitNotActive 27 | } 28 | return time.Parse(dateFormat, value) 29 | } 30 | 31 | // Get the number of times a process restarted (`systemctl show [unit] --property NRestarts`) as an int 32 | func GetNumRestarts(ctx context.Context, unit string, opts Options) (int, error) { 33 | value, err := Show(ctx, unit, properties.NRestarts, opts) 34 | if err != nil { 35 | return -1, err 36 | } 37 | return strconv.Atoi(value) 38 | } 39 | 40 | // Get current memory in bytes (`systemctl show [unit] --property MemoryCurrent`) an an int 41 | func GetMemoryUsage(ctx context.Context, unit string, opts Options) (int, error) { 42 | value, err := Show(ctx, unit, properties.MemoryCurrent, opts) 43 | if err != nil { 44 | return -1, err 45 | } 46 | if value == "[not set]" { 47 | return -1, ErrValueNotSet 48 | } 49 | return strconv.Atoi(value) 50 | } 51 | 52 | // Get the PID of the main process (`systemctl show [unit] --property MainPID`) as an int 53 | func GetPID(ctx context.Context, unit string, opts Options) (int, error) { 54 | value, err := Show(ctx, unit, properties.MainPID, opts) 55 | if err != nil { 56 | return -1, err 57 | } 58 | return strconv.Atoi(value) 59 | } 60 | 61 | func GetSocketsForServiceUnit(ctx context.Context, unit string, opts Options) ([]string, error) { 62 | args := []string{"list-sockets", "--all", "--no-legend", "--no-pager"} 63 | if opts.UserMode { 64 | args = append(args, "--user") 65 | } 66 | stdout, _, _, err := execute(ctx, args) 67 | if err != nil { 68 | return []string{}, err 69 | } 70 | lines := strings.Split(stdout, "\n") 71 | sockets := []string{} 72 | for _, line := range lines { 73 | fields := strings.Fields(line) 74 | if len(fields) < 3 { 75 | continue 76 | } 77 | socketUnit := fields[1] 78 | serviceUnit := fields[2] 79 | if serviceUnit == unit+".service" { 80 | sockets = append(sockets, socketUnit) 81 | } 82 | } 83 | return sockets, nil 84 | } 85 | 86 | func GetUnits(ctx context.Context, opts Options) ([]Unit, error) { 87 | args := []string{"list-units", "--all", "--no-legend", "--full", "--no-pager"} 88 | if opts.UserMode { 89 | args = append(args, "--user") 90 | } 91 | stdout, stderr, _, err := execute(ctx, args) 92 | if err != nil { 93 | return []Unit{}, errors.Join(err, filterErr(stderr)) 94 | } 95 | lines := strings.Split(stdout, "\n") 96 | units := []Unit{} 97 | for _, line := range lines { 98 | entry := strings.Fields(line) 99 | if len(entry) < 4 { 100 | continue 101 | } 102 | unit := Unit{ 103 | Name: entry[0], 104 | Load: entry[1], 105 | Active: entry[2], 106 | Sub: entry[3], 107 | Description: strings.Join(entry[4:], " "), 108 | } 109 | units = append(units, unit) 110 | } 111 | return units, nil 112 | } 113 | 114 | func GetMaskedUnits(ctx context.Context, opts Options) ([]string, error) { 115 | args := []string{"list-unit-files", "--state=masked"} 116 | if opts.UserMode { 117 | args = append(args, "--user") 118 | } 119 | stdout, stderr, _, err := execute(ctx, args) 120 | if err != nil { 121 | return []string{}, errors.Join(err, filterErr(stderr)) 122 | } 123 | lines := strings.Split(stdout, "\n") 124 | units := []string{} 125 | for _, line := range lines { 126 | if !strings.Contains(line, "masked") { 127 | continue 128 | } 129 | entry := strings.Split(line, " ") 130 | if len(entry) < 3 { 131 | continue 132 | } 133 | if entry[1] == "masked" { 134 | unit := entry[0] 135 | uName := strings.Split(unit, ".") 136 | unit = uName[0] 137 | units = append(units, unit) 138 | } 139 | } 140 | return units, nil 141 | } 142 | 143 | // check if systemd is the current init system 144 | func IsSystemd() (bool, error) { 145 | b, err := os.ReadFile("/proc/1/comm") 146 | if err != nil { 147 | return false, err 148 | } 149 | return strings.TrimSpace(string(b)) == "systemd", nil 150 | } 151 | 152 | // check if a service is masked 153 | func IsMasked(ctx context.Context, unit string, opts Options) (bool, error) { 154 | units, err := GetMaskedUnits(ctx, opts) 155 | if err != nil { 156 | return false, err 157 | } 158 | for _, u := range units { 159 | if u == unit { 160 | return true, nil 161 | } 162 | } 163 | return false, nil 164 | } 165 | 166 | // check if a service is running 167 | // https://unix.stackexchange.com/a/396633 168 | func IsRunning(ctx context.Context, unit string, opts Options) (bool, error) { 169 | status, err := Show(ctx, unit, properties.SubState, opts) 170 | return status == "running", err 171 | } 172 | -------------------------------------------------------------------------------- /helpers_test.go: -------------------------------------------------------------------------------- 1 | package systemctl 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "syscall" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | // Testing assumptions 13 | // - there's no unit installed named `nonexistant` 14 | // - the syncthing unit is available but the binary is not on the tester's system 15 | // this is just what was available on mine, should you want to change it, 16 | // either to something in this repo or more common, feel free to submit a PR. 17 | // - your 'user' isn't root 18 | // - your user doesn't have a PolKit rule allowing access to configure nginx 19 | 20 | func TestGetStartTime(t *testing.T) { 21 | if testing.Short() { 22 | t.Skip("skipping in short mode") 23 | } 24 | testCases := []struct { 25 | unit string 26 | err error 27 | opts Options 28 | runAsUser bool 29 | }{ 30 | // Run these tests only as a user 31 | // try nonexistant unit in user mode as user 32 | {"nonexistant", ErrUnitNotActive, Options{UserMode: false}, true}, 33 | // try existing unit in user mode as user 34 | {"syncthing", ErrUnitNotActive, Options{UserMode: true}, true}, 35 | // try existing unit in system mode as user 36 | {"nginx", nil, Options{UserMode: false}, true}, 37 | 38 | // Run these tests only as a superuser 39 | 40 | // try nonexistant unit in system mode as system 41 | {"nonexistant", ErrUnitNotActive, Options{UserMode: false}, false}, 42 | // try existing unit in system mode as system 43 | {"nginx", ErrBusFailure, Options{UserMode: true}, false}, 44 | // try existing unit in system mode as system 45 | {"nginx", nil, Options{UserMode: false}, false}, 46 | } 47 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 48 | defer cancel() 49 | Restart(ctx, "syncthing", Options{UserMode: true}) 50 | Stop(ctx, "syncthing", Options{UserMode: true}) 51 | time.Sleep(1 * time.Second) 52 | for _, tc := range testCases { 53 | t.Run(fmt.Sprintf("%s as %s, UserMode=%v", tc.unit, userString, tc.opts.UserMode), func(t *testing.T) { 54 | if (userString == "root" || userString == "system") && tc.runAsUser { 55 | t.Skip("skipping user test while running as superuser") 56 | } else if (userString != "root" && userString != "system") && !tc.runAsUser { 57 | t.Skip("skipping superuser test while running as user") 58 | } 59 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 60 | defer cancel() 61 | _, err := GetStartTime(ctx, tc.unit, tc.opts) 62 | if !errors.Is(err, tc.err) { 63 | t.Errorf("error is %v, but should have been %v", err, tc.err) 64 | } 65 | }) 66 | } 67 | // Prove start time changes after a restart 68 | t.Run("prove start time changes", func(t *testing.T) { 69 | if userString != "root" && userString != "system" { 70 | t.Skip("skipping superuser test while running as user") 71 | } 72 | 73 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 74 | defer cancel() 75 | startTime, err := GetStartTime(ctx, "nginx", Options{UserMode: false}) 76 | if err != nil { 77 | t.Errorf("issue getting start time of nginx: %v", err) 78 | } 79 | time.Sleep(1 * time.Second) 80 | err = Restart(ctx, "nginx", Options{UserMode: false}) 81 | if err != nil { 82 | t.Errorf("issue restarting nginx as %s: %v", userString, err) 83 | } 84 | time.Sleep(100 * time.Millisecond) 85 | newStartTime, err := GetStartTime(ctx, "nginx", Options{UserMode: false}) 86 | if err != nil { 87 | t.Errorf("issue getting second start time of nginx: %v", err) 88 | } 89 | diff := newStartTime.Sub(startTime).Seconds() 90 | if diff <= 0 { 91 | t.Errorf("Expected start diff to be positive, but got: %d", int(diff)) 92 | } 93 | }) 94 | } 95 | 96 | func TestGetNumRestarts(t *testing.T) { 97 | type testCase struct { 98 | unit string 99 | err error 100 | opts Options 101 | runAsUser bool 102 | } 103 | testCases := []testCase{ 104 | // Run these tests only as a user 105 | 106 | // try nonexistant unit in user mode as user 107 | {"nonexistant", ErrValueNotSet, Options{UserMode: false}, true}, 108 | // try existing unit in user mode as user 109 | {"syncthing", ErrValueNotSet, Options{UserMode: true}, true}, 110 | // try existing unit in system mode as user 111 | {"nginx", nil, Options{UserMode: false}, true}, 112 | 113 | // Run these tests only as a superuser 114 | 115 | // try nonexistant unit in system mode as system 116 | {"nonexistant", ErrValueNotSet, Options{UserMode: false}, false}, 117 | // try existing unit in system mode as system 118 | {"nginx", ErrBusFailure, Options{UserMode: true}, false}, 119 | // try existing unit in system mode as system 120 | {"nginx", nil, Options{UserMode: false}, false}, 121 | } 122 | for _, tc := range testCases { 123 | func(tc testCase) { 124 | t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) { 125 | t.Parallel() 126 | if (userString == "root" || userString == "system") && tc.runAsUser { 127 | t.Skip("skipping user test while running as superuser") 128 | } else if (userString != "root" && userString != "system") && !tc.runAsUser { 129 | t.Skip("skipping superuser test while running as user") 130 | } 131 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 132 | defer cancel() 133 | _, err := GetNumRestarts(ctx, tc.unit, tc.opts) 134 | if !errors.Is(err, tc.err) { 135 | t.Errorf("error is %v, but should have been %v", err, tc.err) 136 | } 137 | }) 138 | }(tc) 139 | } 140 | // Prove restart count increases by one after a restart 141 | t.Run("prove restart count increases by one after a restart", func(t *testing.T) { 142 | if testing.Short() { 143 | t.Skip("skipping in short mode") 144 | } 145 | if userString != "root" && userString != "system" { 146 | t.Skip("skipping superuser test while running as user") 147 | } 148 | 149 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 150 | defer cancel() 151 | restarts, err := GetNumRestarts(ctx, "nginx", Options{UserMode: false}) 152 | if err != nil { 153 | t.Errorf("issue getting number of restarts for nginx: %v", err) 154 | } 155 | pid, err := GetPID(ctx, "nginx", Options{UserMode: false}) 156 | if err != nil { 157 | t.Errorf("issue getting MainPID for nginx as %s: %v", userString, err) 158 | } 159 | syscall.Kill(pid, syscall.SIGKILL) 160 | for { 161 | running, errIsActive := IsActive(ctx, "nginx", Options{UserMode: false}) 162 | if errIsActive != nil { 163 | t.Errorf("error asserting nginx is up: %v", errIsActive) 164 | break 165 | } else if running { 166 | break 167 | } 168 | } 169 | secondRestarts, err := GetNumRestarts(ctx, "nginx", Options{UserMode: false}) 170 | if err != nil { 171 | t.Errorf("issue getting second reading on number of restarts for nginx: %v", err) 172 | } 173 | if restarts+1 != secondRestarts { 174 | t.Errorf("Expected restart count to differ by one, but difference was: %d", secondRestarts-restarts) 175 | } 176 | }) 177 | } 178 | 179 | func TestGetMemoryUsage(t *testing.T) { 180 | type testCase struct { 181 | unit string 182 | err error 183 | opts Options 184 | runAsUser bool 185 | } 186 | testCases := []testCase{ 187 | // Run these tests only as a user 188 | 189 | // try nonexistant unit in user mode as user 190 | {"nonexistant", ErrValueNotSet, Options{UserMode: false}, true}, 191 | // try existing unit in user mode as user 192 | {"syncthing", ErrValueNotSet, Options{UserMode: true}, true}, 193 | // try existing unit in system mode as user 194 | {"nginx", nil, Options{UserMode: false}, true}, 195 | 196 | // Run these tests only as a superuser 197 | 198 | // try nonexistant unit in system mode as system 199 | {"nonexistant", ErrValueNotSet, Options{UserMode: false}, false}, 200 | // try existing unit in system mode as system 201 | {"nginx", ErrBusFailure, Options{UserMode: true}, false}, 202 | // try existing unit in system mode as system 203 | {"nginx", nil, Options{UserMode: false}, false}, 204 | } 205 | for _, tc := range testCases { 206 | func(tc testCase) { 207 | t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) { 208 | t.Parallel() 209 | if (userString == "root" || userString == "system") && tc.runAsUser { 210 | t.Skip("skipping user test while running as superuser") 211 | } else if (userString != "root" && userString != "system") && !tc.runAsUser { 212 | t.Skip("skipping superuser test while running as user") 213 | } 214 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 215 | defer cancel() 216 | _, err := GetMemoryUsage(ctx, tc.unit, tc.opts) 217 | if !errors.Is(err, tc.err) { 218 | t.Errorf("error is %v, but should have been %v", err, tc.err) 219 | } 220 | }) 221 | }(tc) 222 | } 223 | // Prove memory usage values change across services 224 | t.Run("prove memory usage values change across services", func(t *testing.T) { 225 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 226 | defer cancel() 227 | bytes, err := GetMemoryUsage(ctx, "nginx", Options{UserMode: false}) 228 | if err != nil { 229 | t.Errorf("issue getting memory usage of nginx: %v", err) 230 | } 231 | secondBytes, err := GetMemoryUsage(ctx, "user.slice", Options{UserMode: false}) 232 | if err != nil { 233 | t.Errorf("issue getting second memory usage reading of nginx: %v", err) 234 | } 235 | if bytes == secondBytes { 236 | t.Errorf("Expected memory usage between nginx and user.slice to differ, but both were: %d", bytes) 237 | } 238 | }) 239 | } 240 | 241 | func TestGetUnits(t *testing.T) { 242 | type testCase struct { 243 | err error 244 | runAsUser bool 245 | opts Options 246 | } 247 | testCases := []testCase{{ 248 | // Run these tests only as a user 249 | runAsUser: true, 250 | opts: Options{UserMode: true}, 251 | err: nil, 252 | }} 253 | for _, tc := range testCases { 254 | t.Run(fmt.Sprintf("as %s", userString), func(t *testing.T) { 255 | if (userString == "root" || userString == "system") && tc.runAsUser { 256 | t.Skip("skipping user test while running as superuser") 257 | } else if (userString != "root" && userString != "system") && !tc.runAsUser { 258 | t.Skip("skipping superuser test while running as user") 259 | } 260 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 261 | defer cancel() 262 | units, err := GetUnits(ctx, tc.opts) 263 | if !errors.Is(err, tc.err) { 264 | t.Errorf("error is %v, but should have been %v", err, tc.err) 265 | } 266 | if len(units) == 0 { 267 | t.Errorf("Expected at least one unit, but got none") 268 | } 269 | unit := units[0] 270 | if unit.Name == "" { 271 | t.Errorf("Expected unit name to be non-empty, but got empty") 272 | } 273 | if unit.Load == "" { 274 | t.Errorf("Expected unit load state to be non-empty, but got empty") 275 | } 276 | if unit.Active == "" { 277 | t.Errorf("Expected unit active state to be non-empty, but got empty") 278 | } 279 | if unit.Sub == "" { 280 | t.Errorf("Expected unit sub state to be non-empty, but got empty") 281 | } 282 | if unit.Description == "" { 283 | t.Errorf("Expected unit description to be non-empty, but got empty") 284 | } 285 | }) 286 | } 287 | } 288 | 289 | func TestGetPID(t *testing.T) { 290 | type testCase struct { 291 | unit string 292 | err error 293 | opts Options 294 | runAsUser bool 295 | } 296 | 297 | testCases := []testCase{ 298 | // Run these tests only as a user 299 | 300 | // try nonexistant unit in user mode as user 301 | {"nonexistant", nil, Options{UserMode: false}, true}, 302 | // try existing unit in user mode as user 303 | {"syncthing", nil, Options{UserMode: true}, true}, 304 | // try existing unit in system mode as user 305 | {"nginx", nil, Options{UserMode: false}, true}, 306 | 307 | // Run these tests only as a superuser 308 | 309 | // try nonexistant unit in system mode as system 310 | {"nonexistant", nil, Options{UserMode: false}, false}, 311 | // try existing unit in system mode as system 312 | {"nginx", ErrBusFailure, Options{UserMode: true}, false}, 313 | // try existing unit in system mode as system 314 | {"nginx", nil, Options{UserMode: false}, false}, 315 | } 316 | for _, tc := range testCases { 317 | func(tc testCase) { 318 | t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) { 319 | t.Parallel() 320 | if (userString == "root" || userString == "system") && tc.runAsUser { 321 | t.Skip("skipping user test while running as superuser") 322 | } else if (userString != "root" && userString != "system") && !tc.runAsUser { 323 | t.Skip("skipping superuser test while running as user") 324 | } 325 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 326 | defer cancel() 327 | _, err := GetPID(ctx, tc.unit, tc.opts) 328 | if !errors.Is(err, tc.err) { 329 | t.Errorf("error is %v, but should have been %v", err, tc.err) 330 | } 331 | }) 332 | }(tc) 333 | } 334 | t.Run("prove pid changes", func(t *testing.T) { 335 | if testing.Short() { 336 | t.Skip("skipping in short mode") 337 | } 338 | if userString != "root" && userString != "system" { 339 | t.Skip("skipping superuser test while running as user") 340 | } 341 | unit := "nginx" 342 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 343 | defer cancel() 344 | Restart(ctx, unit, Options{UserMode: true}) 345 | pid, err := GetPID(ctx, unit, Options{UserMode: false}) 346 | if err != nil { 347 | t.Errorf("issue getting MainPID for nginx as %s: %v", userString, err) 348 | } 349 | syscall.Kill(pid, syscall.SIGKILL) 350 | secondPid, err := GetPID(ctx, unit, Options{UserMode: false}) 351 | if err != nil { 352 | t.Errorf("issue getting second MainPID for nginx as %s: %v", userString, err) 353 | } 354 | if pid == secondPid { 355 | t.Errorf("Expected pid != secondPid, but both were: %d", pid) 356 | } 357 | }) 358 | } 359 | -------------------------------------------------------------------------------- /properties/properties.go: -------------------------------------------------------------------------------- 1 | package properties 2 | 3 | type Property string 4 | 5 | const ( 6 | Accept Property = "Accept" 7 | ActiveEnterTimestamp Property = "ActiveEnterTimestamp" 8 | ActiveEnterTimestampMonotonic Property = "ActiveEnterTimestampMonotonic" 9 | ActiveExitTimestampMonotonic Property = "ActiveExitTimestampMonotonic" 10 | ActiveState Property = "ActiveState" 11 | After Property = "After" 12 | AllowIsolate Property = "AllowIsolate" 13 | AssertResult Property = "AssertResult" 14 | AssertTimestamp Property = "AssertTimestamp" 15 | AssertTimestampMonotonic Property = "AssertTimestampMonotonic" 16 | Backlog Property = "Backlog" 17 | Before Property = "Before" 18 | BindIPv6Only Property = "BindIPv6Only" 19 | BindLogSockets Property = "BindLogSockets" 20 | BlockIOAccounting Property = "BlockIOAccounting" 21 | BlockIOWeight Property = "BlockIOWeight" 22 | Broadcast Property = "Broadcast" 23 | CPUAccounting Property = "CPUAccounting" 24 | CPUAffinityFromNUMA Property = "CPUAffinityFromNUMA" 25 | CPUQuotaPerSecUSec Property = "CPUQuotaPerSecUSec" 26 | CPUQuotaPeriodUSec Property = "CPUQuotaPeriodUSec" 27 | CPUSchedulingPolicy Property = "CPUSchedulingPolicy" 28 | CPUSchedulingPriority Property = "CPUSchedulingPriority" 29 | CPUSchedulingResetOnFork Property = "CPUSchedulingResetOnFork" 30 | CPUShares Property = "CPUShares" 31 | CPUUsageNSec Property = "CPUUsageNSec" 32 | CPUWeight Property = "CPUWeight" 33 | CacheDirectoryMode Property = "CacheDirectoryMode" 34 | CanFreeze Property = "CanFreeze" 35 | CanIsolate Property = "CanIsolate" 36 | CanLiveMount Property = "CanLiveMount" 37 | CanReload Property = "CanReload" 38 | CanStart Property = "CanStart" 39 | CanStop Property = "CanStop" 40 | CapabilityBoundingSet Property = "CapabilityBoundingSet" 41 | CleanResult Property = "CleanResult" 42 | CollectMode Property = "CollectMode" 43 | ConditionResult Property = "ConditionResult" 44 | ConditionTimestamp Property = "ConditionTimestamp" 45 | ConditionTimestampMonotonic Property = "ConditionTimestampMonotonic" 46 | ConfigurationDirectoryMode Property = "ConfigurationDirectoryMode" 47 | Conflicts Property = "Conflicts" 48 | ControlGroup Property = "ControlGroup" 49 | ControlGroupId Property = "ControlGroupId" 50 | ControlPID Property = "ControlPID" 51 | CoredumpFilter Property = "CoredumpFilter" 52 | CoredumpReceive Property = "CoredumpReceive" 53 | DebugInvocation Property = "DebugInvocation" 54 | DefaultDependencies Property = "DefaultDependencies" 55 | DefaultMemoryLow Property = "DefaultMemoryLow" 56 | DefaultMemoryMin Property = "DefaultMemoryMin" 57 | DefaultStartupMemoryLow Property = "DefaultStartupMemoryLow" 58 | DeferAcceptUSec Property = "DeferAcceptUSec" 59 | Delegate Property = "Delegate" 60 | Description Property = "Description" 61 | DevicePolicy Property = "DevicePolicy" 62 | DirectoryMode Property = "DirectoryMode" 63 | DynamicUser Property = "DynamicUser" 64 | EffectiveCPUs Property = "EffectiveCPUs" 65 | EffectiveMemoryHigh Property = "EffectiveMemoryHigh" 66 | EffectiveMemoryMax Property = "EffectiveMemoryMax" 67 | EffectiveMemoryNodes Property = "EffectiveMemoryNodes" 68 | EffectiveTasksMax Property = "EffectiveTasksMax" 69 | ExecMainCode Property = "ExecMainCode" 70 | ExecMainExitTimestampMonotonic Property = "ExecMainExitTimestampMonotonic" 71 | ExecMainPID Property = "ExecMainPID" 72 | ExecMainStartTimestamp Property = "ExecMainStartTimestamp" 73 | ExecMainStartTimestampMonotonic Property = "ExecMainStartTimestampMonotonic" 74 | ExecMainStatus Property = "ExecMainStatus" 75 | ExecReload Property = "ExecReload" 76 | ExecReloadEx Property = "ExecReloadEx" 77 | ExecStart Property = "ExecStart" 78 | ExecStartEx Property = "ExecStartEx" 79 | ExtensionImagePolicy Property = "ExtensionImagePolicy" 80 | FailureAction Property = "FailureAction" 81 | FileDescriptorName Property = "FileDescriptorName" 82 | FileDescriptorStoreMax Property = "FileDescriptorStoreMax" 83 | FinalKillSignal Property = "FinalKillSignal" 84 | FlushPending Property = "FlushPending" 85 | FragmentPath Property = "FragmentPath" 86 | FreeBind Property = "FreeBind" 87 | FreezerState Property = "FreezerState" 88 | GID Property = "GID" 89 | GuessMainPID Property = "GuessMainPID" 90 | IOAccounting Property = "IOAccounting" 91 | IOReadBytes Property = "IOReadBytes" 92 | IOReadOperations Property = "IOReadOperations" 93 | IOSchedulingClass Property = "IOSchedulingClass" 94 | IOSchedulingPriority Property = "IOSchedulingPriority" 95 | IOWeight Property = "IOWeight" 96 | IOWriteBytes Property = "IOWriteBytes" 97 | IOWriteOperations Property = "IOWriteOperations" 98 | IPAccounting Property = "IPAccounting" 99 | IPEgressBytes Property = "IPEgressBytes" 100 | IPEgressPackets Property = "IPEgressPackets" 101 | IPIngressBytes Property = "IPIngressBytes" 102 | IPIngressPackets Property = "IPIngressPackets" 103 | IPTOS Property = "IPTOS" 104 | IPTTL Property = "IPTTL" 105 | Id Property = "Id" 106 | IgnoreOnIsolate Property = "IgnoreOnIsolate" 107 | IgnoreSIGPIPE Property = "IgnoreSIGPIPE" 108 | InactiveEnterTimestampMonotonic Property = "InactiveEnterTimestampMonotonic" 109 | InactiveExitTimestamp Property = "InactiveExitTimestamp" 110 | InactiveExitTimestampMonotonic Property = "InactiveExitTimestampMonotonic" 111 | InvocationID Property = "InvocationID" 112 | JobRunningTimeoutUSec Property = "JobRunningTimeoutUSec" 113 | JobTimeoutAction Property = "JobTimeoutAction" 114 | JobTimeoutUSec Property = "JobTimeoutUSec" 115 | KeepAlive Property = "KeepAlive" 116 | KeepAliveIntervalUSec Property = "KeepAliveIntervalUSec" 117 | KeepAliveProbes Property = "KeepAliveProbes" 118 | KeepAliveTimeUSec Property = "KeepAliveTimeUSec" 119 | KeyringMode Property = "KeyringMode" 120 | KillMode Property = "KillMode" 121 | KillSignal Property = "KillSignal" 122 | LimitAS Property = "LimitAS" 123 | LimitASSoft Property = "LimitASSoft" 124 | LimitCORE Property = "LimitCORE" 125 | LimitCORESoft Property = "LimitCORESoft" 126 | LimitCPU Property = "LimitCPU" 127 | LimitCPUSoft Property = "LimitCPUSoft" 128 | LimitDATA Property = "LimitDATA" 129 | LimitDATASoft Property = "LimitDATASoft" 130 | LimitFSIZE Property = "LimitFSIZE" 131 | LimitFSIZESoft Property = "LimitFSIZESoft" 132 | LimitLOCKS Property = "LimitLOCKS" 133 | LimitLOCKSSoft Property = "LimitLOCKSSoft" 134 | LimitMEMLOCK Property = "LimitMEMLOCK" 135 | LimitMEMLOCKSoft Property = "LimitMEMLOCKSoft" 136 | LimitMSGQUEUE Property = "LimitMSGQUEUE" 137 | LimitMSGQUEUESoft Property = "LimitMSGQUEUESoft" 138 | LimitNICE Property = "LimitNICE" 139 | LimitNICESoft Property = "LimitNICESoft" 140 | LimitNOFILE Property = "LimitNOFILE" 141 | LimitNOFILESoft Property = "LimitNOFILESoft" 142 | LimitNPROC Property = "LimitNPROC" 143 | LimitNPROCSoft Property = "LimitNPROCSoft" 144 | LimitRSS Property = "LimitRSS" 145 | LimitRSSSoft Property = "LimitRSSSoft" 146 | LimitRTPRIO Property = "LimitRTPRIO" 147 | LimitRTPRIOSoft Property = "LimitRTPRIOSoft" 148 | LimitRTTIME Property = "LimitRTTIME" 149 | LimitRTTIMESoft Property = "LimitRTTIMESoft" 150 | LimitSIGPENDING Property = "LimitSIGPENDING" 151 | LimitSIGPENDINGSoft Property = "LimitSIGPENDINGSoft" 152 | LimitSTACK Property = "LimitSTACK" 153 | LimitSTACKSoft Property = "LimitSTACKSoft" 154 | Listen Property = "Listen" 155 | LoadState Property = "LoadState" 156 | LockPersonality Property = "LockPersonality" 157 | LogLevelMax Property = "LogLevelMax" 158 | LogRateLimitBurst Property = "LogRateLimitBurst" 159 | LogRateLimitIntervalUSec Property = "LogRateLimitIntervalUSec" 160 | LogsDirectoryMode Property = "LogsDirectoryMode" 161 | MainPID Property = "MainPID" 162 | ManagedOOMMemoryPressure Property = "ManagedOOMMemoryPressure" 163 | ManagedOOMMemoryPressureDurationUSec Property = "ManagedOOMMemoryPressureDurationUSec" 164 | ManagedOOMMemoryPressureLimit Property = "ManagedOOMMemoryPressureLimit" 165 | ManagedOOMPreference Property = "ManagedOOMPreference" 166 | ManagedOOMSwap Property = "ManagedOOMSwap" 167 | Mark Property = "Mark" 168 | MaxConnections Property = "MaxConnections" 169 | MaxConnectionsPerSource Property = "MaxConnectionsPerSource" 170 | MemoryAccounting Property = "MemoryAccounting" 171 | MemoryAvailable Property = "MemoryAvailable" 172 | MemoryCurrent Property = "MemoryCurrent" 173 | MemoryDenyWriteExecute Property = "MemoryDenyWriteExecute" 174 | MemoryHigh Property = "MemoryHigh" 175 | MemoryKSM Property = "MemoryKSM" 176 | MemoryLimit Property = "MemoryLimit" 177 | MemoryLow Property = "MemoryLow" 178 | MemoryMax Property = "MemoryMax" 179 | MemoryMin Property = "MemoryMin" 180 | MemoryPeak Property = "MemoryPeak" 181 | MemoryPressureThresholdUSec Property = "MemoryPressureThresholdUSec" 182 | MemoryPressureWatch Property = "MemoryPressureWatch" 183 | MemorySwapCurrent Property = "MemorySwapCurrent" 184 | MemorySwapMax Property = "MemorySwapMax" 185 | MemorySwapPeak Property = "MemorySwapPeak" 186 | MemoryZSwapCurrent Property = "MemoryZSwapCurrent" 187 | MemoryZSwapMax Property = "MemoryZSwapMax" 188 | MemoryZSwapWriteback Property = "MemoryZSwapWriteback" 189 | MessageQueueMaxMessages Property = "MessageQueueMaxMessages" 190 | MessageQueueMessageSize Property = "MessageQueueMessageSize" 191 | MountAPIVFS Property = "MountAPIVFS" 192 | MountImagePolicy Property = "MountImagePolicy" 193 | NAccepted Property = "NAccepted" 194 | NConnections Property = "NConnections" 195 | NFileDescriptorStore Property = "NFileDescriptorStore" 196 | NRefused Property = "NRefused" 197 | NRestarts Property = "NRestarts" 198 | NUMAPolicy Property = "NUMAPolicy" 199 | Names Property = "Names" 200 | NeedDaemonReload Property = "NeedDaemonReload" 201 | Nice Property = "Nice" 202 | NoDelay Property = "NoDelay" 203 | NoNewPrivileges Property = "NoNewPrivileges" 204 | NonBlocking Property = "NonBlocking" 205 | NotifyAccess Property = "NotifyAccess" 206 | OOMPolicy Property = "OOMPolicy" 207 | OOMScoreAdjust Property = "OOMScoreAdjust" 208 | OnFailureJobMode Property = "OnFailureJobMode" 209 | OnSuccessJobMode Property = "OnSuccessJobMode" 210 | PIDFile Property = "PIDFile" 211 | PassCredentials Property = "PassCredentials" 212 | PassFileDescriptorsToExec Property = "PassFileDescriptorsToExec" 213 | PassPacketInfo Property = "PassPacketInfo" 214 | PassSecurity Property = "PassSecurity" 215 | Perpetual Property = "Perpetual" 216 | PipeSize Property = "PipeSize" 217 | PollLimitBurst Property = "PollLimitBurst" 218 | PollLimitIntervalUSec Property = "PollLimitIntervalUSec" 219 | Priority Property = "Priority" 220 | PrivateDevices Property = "PrivateDevices" 221 | PrivateIPC Property = "PrivateIPC" 222 | PrivateMounts Property = "PrivateMounts" 223 | PrivateNetwork Property = "PrivateNetwork" 224 | PrivatePIDs Property = "PrivatePIDs" 225 | PrivateTmp Property = "PrivateTmp" 226 | PrivateTmpEx Property = "PrivateTmpEx" 227 | PrivateUsers Property = "PrivateUsers" 228 | PrivateUsersEx Property = "PrivateUsersEx" 229 | ProcSubset Property = "ProcSubset" 230 | ProtectClock Property = "ProtectClock" 231 | ProtectControlGroups Property = "ProtectControlGroups" 232 | ProtectControlGroupsEx Property = "ProtectControlGroupsEx" 233 | ProtectHome Property = "ProtectHome" 234 | ProtectHostname Property = "ProtectHostname" 235 | ProtectKernelLogs Property = "ProtectKernelLogs" 236 | ProtectKernelModules Property = "ProtectKernelModules" 237 | ProtectKernelTunables Property = "ProtectKernelTunables" 238 | ProtectProc Property = "ProtectProc" 239 | ProtectSystem Property = "ProtectSystem" 240 | ReceiveBuffer Property = "ReceiveBuffer" 241 | RefuseManualStart Property = "RefuseManualStart" 242 | RefuseManualStop Property = "RefuseManualStop" 243 | ReloadResult Property = "ReloadResult" 244 | RemainAfterExit Property = "RemainAfterExit" 245 | RemoveIPC Property = "RemoveIPC" 246 | RemoveOnStop Property = "RemoveOnStop" 247 | RequiredBy Property = "RequiredBy" 248 | Requires Property = "Requires" 249 | RequiresMountsFor Property = "RequiresMountsFor" 250 | Restart Property = "Restart" 251 | RestartKillSignal Property = "RestartKillSignal" 252 | RestartUSec Property = "RestartUSec" 253 | RestrictNamespaces Property = "RestrictNamespaces" 254 | RestrictRealtime Property = "RestrictRealtime" 255 | RestrictSUIDSGID Property = "RestrictSUIDSGID" 256 | Result Property = "Result" 257 | ReusePort Property = "ReusePort" 258 | RootDirectoryStartOnly Property = "RootDirectoryStartOnly" 259 | RootEphemeral Property = "RootEphemeral" 260 | RootImagePolicy Property = "RootImagePolicy" 261 | RuntimeDirectoryMode Property = "RuntimeDirectoryMode" 262 | RuntimeDirectoryPreserve Property = "RuntimeDirectoryPreserve" 263 | RuntimeMaxUSec Property = "RuntimeMaxUSec" 264 | SameProcessGroup Property = "SameProcessGroup" 265 | SecureBits Property = "SecureBits" 266 | SendBuffer Property = "SendBuffer" 267 | SendSIGHUP Property = "SendSIGHUP" 268 | SendSIGKILL Property = "SendSIGKILL" 269 | SetLoginEnvironment Property = "SetLoginEnvironment" 270 | Slice Property = "Slice" 271 | SocketMode Property = "SocketMode" 272 | SocketProtocol Property = "SocketProtocol" 273 | StandardError Property = "StandardError" 274 | StandardInput Property = "StandardInput" 275 | StandardOutput Property = "StandardOutput" 276 | StartLimitAction Property = "StartLimitAction" 277 | StartLimitBurst Property = "StartLimitBurst" 278 | StartLimitIntervalUSec Property = "StartLimitIntervalUSec" 279 | StartupBlockIOWeight Property = "StartupBlockIOWeight" 280 | StartupCPUShares Property = "StartupCPUShares" 281 | StartupCPUWeight Property = "StartupCPUWeight" 282 | StartupIOWeight Property = "StartupIOWeight" 283 | StartupMemoryHigh Property = "StartupMemoryHigh" 284 | StartupMemoryLow Property = "StartupMemoryLow" 285 | StartupMemoryMax Property = "StartupMemoryMax" 286 | StartupMemorySwapMax Property = "StartupMemorySwapMax" 287 | StartupMemoryZSwapMax Property = "StartupMemoryZSwapMax" 288 | StateChangeTimestamp Property = "StateChangeTimestamp" 289 | StateChangeTimestampMonotonic Property = "StateChangeTimestampMonotonic" 290 | StateDirectoryMode Property = "StateDirectoryMode" 291 | StatusErrno Property = "StatusErrno" 292 | StopWhenUnneeded Property = "StopWhenUnneeded" 293 | SubState Property = "SubState" 294 | SuccessAction Property = "SuccessAction" 295 | SurviveFinalKillSignal Property = "SurviveFinalKillSignal" 296 | SyslogFacility Property = "SyslogFacility" 297 | SyslogLevel Property = "SyslogLevel" 298 | SyslogLevelPrefix Property = "SyslogLevelPrefix" 299 | SyslogPriority Property = "SyslogPriority" 300 | SystemCallErrorNumber Property = "SystemCallErrorNumber" 301 | TTYReset Property = "TTYReset" 302 | TTYVHangup Property = "TTYVHangup" 303 | TTYVTDisallocate Property = "TTYVTDisallocate" 304 | TasksAccounting Property = "TasksAccounting" 305 | TasksCurrent Property = "TasksCurrent" 306 | TasksMax Property = "TasksMax" 307 | TimeoutAbortUSec Property = "TimeoutAbortUSec" 308 | TimeoutCleanUSec Property = "TimeoutCleanUSec" 309 | TimeoutStartFailureMode Property = "TimeoutStartFailureMode" 310 | TimeoutStartUSec Property = "TimeoutStartUSec" 311 | TimeoutStopFailureMode Property = "TimeoutStopFailureMode" 312 | TimeoutStopUSec Property = "TimeoutStopUSec" 313 | TimeoutUSec Property = "TimeoutUSec" 314 | TimerSlackNSec Property = "TimerSlackNSec" 315 | Timestamping Property = "Timestamping" 316 | Transient Property = "Transient" 317 | Transparent Property = "Transparent" 318 | TriggerLimitBurst Property = "TriggerLimitBurst" 319 | TriggerLimitIntervalUSec Property = "TriggerLimitIntervalUSec" 320 | Triggers Property = "Triggers" 321 | Type Property = "Type" 322 | UID Property = "UID" 323 | UMask Property = "UMask" 324 | UnitFilePreset Property = "UnitFilePreset" 325 | UnitFileState Property = "UnitFileState" 326 | UtmpMode Property = "UtmpMode" 327 | WantedBy Property = "WantedBy" 328 | WatchdogSignal Property = "WatchdogSignal" 329 | WatchdogTimestampMonotonic Property = "WatchdogTimestampMonotonic" 330 | WatchdogUSec Property = "WatchdogUSec" 331 | Writable Property = "Writable" 332 | ) 333 | -------------------------------------------------------------------------------- /properties/properties_list.go: -------------------------------------------------------------------------------- 1 | package properties 2 | 3 | var Properties = []Property{ 4 | Accept, 5 | ActiveEnterTimestamp, 6 | ActiveEnterTimestampMonotonic, 7 | ActiveExitTimestampMonotonic, 8 | ActiveState, 9 | After, 10 | AllowIsolate, 11 | AssertResult, 12 | AssertTimestamp, 13 | AssertTimestampMonotonic, 14 | Backlog, 15 | Before, 16 | BindIPv6Only, 17 | BindLogSockets, 18 | BlockIOAccounting, 19 | BlockIOWeight, 20 | Broadcast, 21 | CPUAccounting, 22 | CPUAffinityFromNUMA, 23 | CPUQuotaPerSecUSec, 24 | CPUQuotaPeriodUSec, 25 | CPUSchedulingPolicy, 26 | CPUSchedulingPriority, 27 | CPUSchedulingResetOnFork, 28 | CPUShares, 29 | CPUUsageNSec, 30 | CPUWeight, 31 | CacheDirectoryMode, 32 | CanFreeze, 33 | CanIsolate, 34 | CanLiveMount, 35 | CanReload, 36 | CanStart, 37 | CanStop, 38 | CapabilityBoundingSet, 39 | CleanResult, 40 | CollectMode, 41 | ConditionResult, 42 | ConditionTimestamp, 43 | ConditionTimestampMonotonic, 44 | ConfigurationDirectoryMode, 45 | Conflicts, 46 | ControlGroup, 47 | ControlGroupId, 48 | ControlPID, 49 | CoredumpFilter, 50 | CoredumpReceive, 51 | DebugInvocation, 52 | DefaultDependencies, 53 | DefaultMemoryLow, 54 | DefaultMemoryMin, 55 | DefaultStartupMemoryLow, 56 | DeferAcceptUSec, 57 | Delegate, 58 | Description, 59 | DevicePolicy, 60 | DirectoryMode, 61 | DynamicUser, 62 | EffectiveCPUs, 63 | EffectiveMemoryHigh, 64 | EffectiveMemoryMax, 65 | EffectiveMemoryNodes, 66 | EffectiveTasksMax, 67 | ExecMainCode, 68 | ExecMainExitTimestampMonotonic, 69 | ExecMainPID, 70 | ExecMainStartTimestamp, 71 | ExecMainStartTimestampMonotonic, 72 | ExecMainStatus, 73 | ExecReload, 74 | ExecReloadEx, 75 | ExecStart, 76 | ExecStartEx, 77 | ExtensionImagePolicy, 78 | FailureAction, 79 | FileDescriptorName, 80 | FileDescriptorStoreMax, 81 | FinalKillSignal, 82 | FlushPending, 83 | FragmentPath, 84 | FreeBind, 85 | FreezerState, 86 | GID, 87 | GuessMainPID, 88 | IOAccounting, 89 | IOReadBytes, 90 | IOReadOperations, 91 | IOSchedulingClass, 92 | IOSchedulingPriority, 93 | IOWeight, 94 | IOWriteBytes, 95 | IOWriteOperations, 96 | IPAccounting, 97 | IPEgressBytes, 98 | IPEgressPackets, 99 | IPIngressBytes, 100 | IPIngressPackets, 101 | IPTOS, 102 | IPTTL, 103 | Id, 104 | IgnoreOnIsolate, 105 | IgnoreSIGPIPE, 106 | InactiveEnterTimestampMonotonic, 107 | InactiveExitTimestamp, 108 | InactiveExitTimestampMonotonic, 109 | InvocationID, 110 | JobRunningTimeoutUSec, 111 | JobTimeoutAction, 112 | JobTimeoutUSec, 113 | KeepAlive, 114 | KeepAliveIntervalUSec, 115 | KeepAliveProbes, 116 | KeepAliveTimeUSec, 117 | KeyringMode, 118 | KillMode, 119 | KillSignal, 120 | LimitAS, 121 | LimitASSoft, 122 | LimitCORE, 123 | LimitCORESoft, 124 | LimitCPU, 125 | LimitCPUSoft, 126 | LimitDATA, 127 | LimitDATASoft, 128 | LimitFSIZE, 129 | LimitFSIZESoft, 130 | LimitLOCKS, 131 | LimitLOCKSSoft, 132 | LimitMEMLOCK, 133 | LimitMEMLOCKSoft, 134 | LimitMSGQUEUE, 135 | LimitMSGQUEUESoft, 136 | LimitNICE, 137 | LimitNICESoft, 138 | LimitNOFILE, 139 | LimitNOFILESoft, 140 | LimitNPROC, 141 | LimitNPROCSoft, 142 | LimitRSS, 143 | LimitRSSSoft, 144 | LimitRTPRIO, 145 | LimitRTPRIOSoft, 146 | LimitRTTIME, 147 | LimitRTTIMESoft, 148 | LimitSIGPENDING, 149 | LimitSIGPENDINGSoft, 150 | LimitSTACK, 151 | LimitSTACKSoft, 152 | Listen, 153 | LoadState, 154 | LockPersonality, 155 | LogLevelMax, 156 | LogRateLimitBurst, 157 | LogRateLimitIntervalUSec, 158 | LogsDirectoryMode, 159 | MainPID, 160 | ManagedOOMMemoryPressure, 161 | ManagedOOMMemoryPressureDurationUSec, 162 | ManagedOOMMemoryPressureLimit, 163 | ManagedOOMPreference, 164 | ManagedOOMSwap, 165 | Mark, 166 | MaxConnections, 167 | MaxConnectionsPerSource, 168 | MemoryAccounting, 169 | MemoryAvailable, 170 | MemoryCurrent, 171 | MemoryDenyWriteExecute, 172 | MemoryHigh, 173 | MemoryKSM, 174 | MemoryLimit, 175 | MemoryLow, 176 | MemoryMax, 177 | MemoryMin, 178 | MemoryPeak, 179 | MemoryPressureThresholdUSec, 180 | MemoryPressureWatch, 181 | MemorySwapCurrent, 182 | MemorySwapMax, 183 | MemorySwapPeak, 184 | MemoryZSwapCurrent, 185 | MemoryZSwapMax, 186 | MemoryZSwapWriteback, 187 | MessageQueueMaxMessages, 188 | MessageQueueMessageSize, 189 | MountAPIVFS, 190 | MountImagePolicy, 191 | NAccepted, 192 | NConnections, 193 | NFileDescriptorStore, 194 | NRefused, 195 | NRestarts, 196 | NUMAPolicy, 197 | Names, 198 | NeedDaemonReload, 199 | Nice, 200 | NoDelay, 201 | NoNewPrivileges, 202 | NonBlocking, 203 | NotifyAccess, 204 | OOMPolicy, 205 | OOMScoreAdjust, 206 | OnFailureJobMode, 207 | OnSuccessJobMode, 208 | PIDFile, 209 | PassCredentials, 210 | PassFileDescriptorsToExec, 211 | PassPacketInfo, 212 | PassSecurity, 213 | Perpetual, 214 | PipeSize, 215 | PollLimitBurst, 216 | PollLimitIntervalUSec, 217 | Priority, 218 | PrivateDevices, 219 | PrivateIPC, 220 | PrivateMounts, 221 | PrivateNetwork, 222 | PrivatePIDs, 223 | PrivateTmp, 224 | PrivateTmpEx, 225 | PrivateUsers, 226 | PrivateUsersEx, 227 | ProcSubset, 228 | ProtectClock, 229 | ProtectControlGroups, 230 | ProtectControlGroupsEx, 231 | ProtectHome, 232 | ProtectHostname, 233 | ProtectKernelLogs, 234 | ProtectKernelModules, 235 | ProtectKernelTunables, 236 | ProtectProc, 237 | ProtectSystem, 238 | ReceiveBuffer, 239 | RefuseManualStart, 240 | RefuseManualStop, 241 | ReloadResult, 242 | RemainAfterExit, 243 | RemoveIPC, 244 | RemoveOnStop, 245 | RequiredBy, 246 | Requires, 247 | RequiresMountsFor, 248 | Restart, 249 | RestartKillSignal, 250 | RestartUSec, 251 | RestrictNamespaces, 252 | RestrictRealtime, 253 | RestrictSUIDSGID, 254 | Result, 255 | ReusePort, 256 | RootDirectoryStartOnly, 257 | RootEphemeral, 258 | RootImagePolicy, 259 | RuntimeDirectoryMode, 260 | RuntimeDirectoryPreserve, 261 | RuntimeMaxUSec, 262 | SameProcessGroup, 263 | SecureBits, 264 | SendBuffer, 265 | SendSIGHUP, 266 | SendSIGKILL, 267 | SetLoginEnvironment, 268 | Slice, 269 | SocketMode, 270 | SocketProtocol, 271 | StandardError, 272 | StandardInput, 273 | StandardOutput, 274 | StartLimitAction, 275 | StartLimitBurst, 276 | StartLimitIntervalUSec, 277 | StartupBlockIOWeight, 278 | StartupCPUShares, 279 | StartupCPUWeight, 280 | StartupIOWeight, 281 | StartupMemoryHigh, 282 | StartupMemoryLow, 283 | StartupMemoryMax, 284 | StartupMemorySwapMax, 285 | StartupMemoryZSwapMax, 286 | StateChangeTimestamp, 287 | StateChangeTimestampMonotonic, 288 | StateDirectoryMode, 289 | StatusErrno, 290 | StopWhenUnneeded, 291 | SubState, 292 | SuccessAction, 293 | SurviveFinalKillSignal, 294 | SyslogFacility, 295 | SyslogLevel, 296 | SyslogLevelPrefix, 297 | SyslogPriority, 298 | SystemCallErrorNumber, 299 | TTYReset, 300 | TTYVHangup, 301 | TTYVTDisallocate, 302 | TasksAccounting, 303 | TasksCurrent, 304 | TasksMax, 305 | TimeoutAbortUSec, 306 | TimeoutCleanUSec, 307 | TimeoutStartFailureMode, 308 | TimeoutStartUSec, 309 | TimeoutStopFailureMode, 310 | TimeoutStopUSec, 311 | TimeoutUSec, 312 | TimerSlackNSec, 313 | Timestamping, 314 | Transient, 315 | Transparent, 316 | TriggerLimitBurst, 317 | TriggerLimitIntervalUSec, 318 | Triggers, 319 | Type, 320 | UID, 321 | UMask, 322 | UnitFilePreset, 323 | UnitFileState, 324 | UtmpMode, 325 | WantedBy, 326 | WatchdogSignal, 327 | WatchdogTimestampMonotonic, 328 | WatchdogUSec, 329 | Writable, 330 | } 331 | -------------------------------------------------------------------------------- /structs.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package systemctl 4 | 5 | type Options struct { 6 | UserMode bool 7 | } 8 | 9 | type Unit struct { 10 | Name string 11 | Load string 12 | Active string 13 | Sub string 14 | Description string 15 | } 16 | 17 | var unitTypes = []string{ 18 | "automount", 19 | "device", 20 | "mount", 21 | "path", 22 | "scope", 23 | "service", 24 | "slice", 25 | "snapshot", 26 | "socket", 27 | "swap", 28 | "target", 29 | "timer", 30 | } 31 | -------------------------------------------------------------------------------- /systemctl.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package systemctl 4 | 5 | import ( 6 | "context" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/taigrr/systemctl/properties" 11 | ) 12 | 13 | // Reload systemd manager configuration. 14 | // 15 | // This will rerun all generators (see systemd. generator(7)), reload all unit 16 | // files, and recreate the entire dependency tree. While the daemon is being 17 | // reloaded, all sockets systemd listens on behalf of user configuration will 18 | // stay accessible. 19 | func DaemonReload(ctx context.Context, opts Options) error { 20 | args := []string{"daemon-reload", "--system"} 21 | if opts.UserMode { 22 | args[1] = "--user" 23 | } 24 | _, _, _, err := execute(ctx, args) 25 | return err 26 | } 27 | 28 | // Reenables one or more units. 29 | // 30 | // This removes all symlinks to the unit files backing the specified units from 31 | // the unit configuration directory, then recreates the symlink to the unit again, 32 | // atomically. Can be used to change the symlink target. 33 | func Reenable(ctx context.Context, unit string, opts Options) error { 34 | args := []string{"reenable", "--system", unit} 35 | if opts.UserMode { 36 | args[1] = "--user" 37 | } 38 | _, _, _, err := execute(ctx, args) 39 | return err 40 | } 41 | 42 | // Disables one or more units. 43 | // 44 | // This removes all symlinks to the unit files backing the specified units from 45 | // the unit configuration directory, and hence undoes any changes made by 46 | // enable or link. 47 | func Disable(ctx context.Context, unit string, opts Options) error { 48 | args := []string{"disable", "--system", unit} 49 | if opts.UserMode { 50 | args[1] = "--user" 51 | } 52 | _, _, _, err := execute(ctx, args) 53 | return err 54 | } 55 | 56 | // Enable one or more units or unit instances. 57 | // 58 | // This will create a set of symlinks, as encoded in the [Install] sections of 59 | // the indicated unit files. After the symlinks have been created, the system 60 | // manager configuration is reloaded (in a way equivalent to daemon-reload), 61 | // in order to ensure the changes are taken into account immediately. 62 | func Enable(ctx context.Context, unit string, opts Options) error { 63 | args := []string{"enable", "--system", unit} 64 | if opts.UserMode { 65 | args[1] = "--user" 66 | } 67 | _, _, _, err := execute(ctx, args) 68 | return err 69 | } 70 | 71 | // Check whether any of the specified units are active (i.e. running). 72 | // 73 | // Returns true if the unit is active, false if inactive or failed. 74 | // Also returns false in an error case. 75 | func IsActive(ctx context.Context, unit string, opts Options) (bool, error) { 76 | args := []string{"is-active", "--system", unit} 77 | if opts.UserMode { 78 | args[1] = "--user" 79 | } 80 | stdout, _, _, err := execute(ctx, args) 81 | stdout = strings.TrimSuffix(stdout, "\n") 82 | switch stdout { 83 | case "inactive": 84 | return false, nil 85 | case "active": 86 | return true, nil 87 | case "failed": 88 | return false, nil 89 | case "activating": 90 | return false, nil 91 | default: 92 | return false, err 93 | } 94 | } 95 | 96 | // Checks whether any of the specified unit files are enabled (as with enable). 97 | // 98 | // Returns true if the unit is enabled, aliased, static, indirect, generated 99 | // or transient. 100 | // 101 | // Returns false if disabled. Also returns an error if linked, masked, or bad. 102 | // 103 | // See https://www.freedesktop.org/software/systemd/man/systemctl.html#is-enabled%20UNIT%E2%80%A6 104 | // for more information 105 | func IsEnabled(ctx context.Context, unit string, opts Options) (bool, error) { 106 | args := []string{"is-enabled", "--system", unit} 107 | if opts.UserMode { 108 | args[1] = "--user" 109 | } 110 | stdout, _, _, err := execute(ctx, args) 111 | stdout = strings.TrimSuffix(stdout, "\n") 112 | switch stdout { 113 | case "enabled": 114 | return true, nil 115 | case "enabled-runtime": 116 | return true, nil 117 | case "linked": 118 | return false, ErrLinked 119 | case "linked-runtime": 120 | return false, ErrLinked 121 | case "alias": 122 | return true, nil 123 | case "masked": 124 | return false, ErrMasked 125 | case "masked-runtime": 126 | return false, ErrMasked 127 | case "static": 128 | return true, nil 129 | case "indirect": 130 | return true, nil 131 | case "disabled": 132 | return false, nil 133 | case "generated": 134 | return true, nil 135 | case "transient": 136 | return true, nil 137 | } 138 | if err != nil { 139 | return false, err 140 | } 141 | return false, ErrUnspecified 142 | } 143 | 144 | // Check whether any of the specified units are in a "failed" state. 145 | func IsFailed(ctx context.Context, unit string, opts Options) (bool, error) { 146 | args := []string{"is-failed", "--system", unit} 147 | if opts.UserMode { 148 | args[1] = "--user" 149 | } 150 | stdout, _, _, err := execute(ctx, args) 151 | if matched, _ := regexp.MatchString(`inactive`, stdout); matched { 152 | return false, nil 153 | } else if matched, _ := regexp.MatchString(`active`, stdout); matched { 154 | return false, nil 155 | } else if matched, _ := regexp.MatchString(`failed`, stdout); matched { 156 | return true, nil 157 | } 158 | return false, err 159 | } 160 | 161 | // Mask one or more units, as specified on the command line. This will link 162 | // these unit files to /dev/null, making it impossible to start them. 163 | // 164 | // Notably, Mask may return ErrDoesNotExist if a unit doesn't exist, but it will 165 | // continue masking anyway. Calling Mask on a non-existing masked unit does not 166 | // return an error. Similarly, see Unmask. 167 | func Mask(ctx context.Context, unit string, opts Options) error { 168 | args := []string{"mask", "--system", unit} 169 | if opts.UserMode { 170 | args[1] = "--user" 171 | } 172 | _, _, _, err := execute(ctx, args) 173 | return err 174 | } 175 | 176 | // Stop and then start one or more units specified on the command line. 177 | // If the units are not running yet, they will be started. 178 | func Restart(ctx context.Context, unit string, opts Options) error { 179 | args := []string{"restart", "--system", unit} 180 | if opts.UserMode { 181 | args[1] = "--user" 182 | } 183 | _, _, _, err := execute(ctx, args) 184 | return err 185 | } 186 | 187 | // Show a selected property of a unit. Accepted properties are predefined in the 188 | // properties subpackage to guarantee properties are valid and assist code-completion. 189 | func Show(ctx context.Context, unit string, property properties.Property, opts Options) (string, error) { 190 | args := []string{"show", "--system", unit, "--property", string(property)} 191 | if opts.UserMode { 192 | args[1] = "--user" 193 | } 194 | stdout, _, _, err := execute(ctx, args) 195 | stdout = strings.TrimPrefix(stdout, string(property)+"=") 196 | stdout = strings.TrimSuffix(stdout, "\n") 197 | return stdout, err 198 | } 199 | 200 | // Start (activate) a given unit 201 | func Start(ctx context.Context, unit string, opts Options) error { 202 | args := []string{"start", "--system", unit} 203 | if opts.UserMode { 204 | args[1] = "--user" 205 | } 206 | _, _, _, err := execute(ctx, args) 207 | return err 208 | } 209 | 210 | // Get back the status string which would be returned by running 211 | // `systemctl status [unit]`. 212 | // 213 | // Generally, it makes more sense to programatically retrieve the properties 214 | // using Show, but this command is provided for the sake of completeness 215 | func Status(ctx context.Context, unit string, opts Options) (string, error) { 216 | args := []string{"status", "--system", unit} 217 | if opts.UserMode { 218 | args[1] = "--user" 219 | } 220 | stdout, _, _, err := execute(ctx, args) 221 | return stdout, err 222 | } 223 | 224 | // Stop (deactivate) a given unit 225 | func Stop(ctx context.Context, unit string, opts Options) error { 226 | args := []string{"stop", "--system", unit} 227 | if opts.UserMode { 228 | args[1] = "--user" 229 | } 230 | _, _, _, err := execute(ctx, args) 231 | return err 232 | } 233 | 234 | // Unmask one or more unit files, as specified on the command line. 235 | // This will undo the effect of Mask. 236 | // 237 | // In line with systemd, Unmask will return ErrDoesNotExist if the unit 238 | // doesn't exist, but only if it's not already masked. 239 | // If the unit doesn't exist but it's masked anyway, no error will be 240 | // returned. Gross, I know. Take it up with Poettering. 241 | func Unmask(ctx context.Context, unit string, opts Options) error { 242 | args := []string{"unmask", "--system", unit} 243 | if opts.UserMode { 244 | args[1] = "--user" 245 | } 246 | _, _, _, err := execute(ctx, args) 247 | return err 248 | } 249 | -------------------------------------------------------------------------------- /systemctl_test.go: -------------------------------------------------------------------------------- 1 | package systemctl 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "os/user" 9 | "syscall" 10 | "testing" 11 | "time" 12 | 13 | "github.com/taigrr/systemctl/properties" 14 | ) 15 | 16 | var userString string 17 | 18 | // Testing assumptions 19 | // - there's no unit installed named `nonexistant` 20 | // - the syncthing unit to be available on the tester's system. 21 | // this is just what was available on mine, should you want to change it, 22 | // either to something in this repo or more common, feel free to submit a PR. 23 | // - your 'user' isn't root 24 | // - your user doesn't have a PolKit rule allowing access to configure nginx 25 | 26 | func TestMain(m *testing.M) { 27 | curUser, err := user.Current() 28 | if err != nil { 29 | fmt.Println("Could not determine running user") 30 | } 31 | userString = curUser.Username 32 | fmt.Printf("currently running tests as: %s \n", userString) 33 | fmt.Println("Don't forget to run both root and user tests.") 34 | retCode := m.Run() 35 | if userString == "root" || userString == "system" { 36 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 37 | defer cancel() 38 | Restart(ctx, "nginx", Options{UserMode: false}) 39 | } 40 | os.Exit(retCode) 41 | } 42 | 43 | func TestDaemonReload(t *testing.T) { 44 | testCases := []struct { 45 | err error 46 | opts Options 47 | runAsUser bool 48 | }{ 49 | /* Run these tests only as a user */ 50 | 51 | // fail to reload system daemon as user 52 | {ErrInsufficientPermissions, Options{UserMode: false}, true}, 53 | // reload user's scope daemon 54 | {nil, Options{UserMode: true}, true}, 55 | /* End user tests*/ 56 | 57 | /* Run these tests only as a superuser */ 58 | 59 | // succeed to reload daemon 60 | {nil, Options{UserMode: false}, false}, 61 | // fail to connect to user bus as system 62 | {ErrBusFailure, Options{UserMode: true}, false}, 63 | 64 | /* End superuser tests*/ 65 | } 66 | for _, tc := range testCases { 67 | mode := "user" 68 | if tc.opts.UserMode == false { 69 | mode = "system" 70 | } 71 | t.Run(fmt.Sprintf("DaemonReload as %s, %s mode", userString, mode), func(t *testing.T) { 72 | if (userString == "root" || userString == "system") && tc.runAsUser { 73 | t.Skip("skipping user test while running as superuser") 74 | } else if (userString != "root" && userString != "system") && !tc.runAsUser { 75 | t.Skip("skipping superuser test while running as user") 76 | } 77 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 78 | defer cancel() 79 | err := DaemonReload(ctx, tc.opts) 80 | if !errors.Is(err, tc.err) { 81 | t.Errorf("error is %v, but should have been %v", err, tc.err) 82 | } 83 | }) 84 | } 85 | } 86 | 87 | func TestDisable(t *testing.T) { 88 | if userString != "root" && userString != "system" { 89 | t.Skip("skipping superuser test while running as user") 90 | } 91 | unit := "nginx" 92 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 93 | defer cancel() 94 | err := Mask(ctx, unit, Options{UserMode: false}) 95 | if err != nil { 96 | Unmask(ctx, unit, Options{UserMode: false}) 97 | t.Errorf("Unable to mask %s", unit) 98 | } 99 | err = Disable(ctx, unit, Options{UserMode: false}) 100 | if !errors.Is(err, ErrMasked) { 101 | Unmask(ctx, unit, Options{UserMode: false}) 102 | t.Errorf("error is %v, but should have been %v", err, ErrMasked) 103 | } 104 | err = Unmask(ctx, unit, Options{UserMode: false}) 105 | if err != nil { 106 | t.Errorf("Unable to unmask %s", unit) 107 | } 108 | } 109 | 110 | func TestReenable(t *testing.T) { 111 | if userString != "root" && userString != "system" { 112 | t.Skip("skipping superuser test while running as user") 113 | } 114 | unit := "nginx" 115 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 116 | defer cancel() 117 | err := Mask(ctx, unit, Options{UserMode: false}) 118 | if err != nil { 119 | Unmask(ctx, unit, Options{UserMode: false}) 120 | t.Errorf("Unable to mask %s", unit) 121 | } 122 | err = Reenable(ctx, unit, Options{UserMode: false}) 123 | if !errors.Is(err, ErrMasked) { 124 | Unmask(ctx, unit, Options{UserMode: false}) 125 | t.Errorf("error is %v, but should have been %v", err, ErrMasked) 126 | } 127 | err = Unmask(ctx, unit, Options{UserMode: false}) 128 | if err != nil { 129 | t.Errorf("Unable to unmask %s", unit) 130 | } 131 | } 132 | 133 | func TestEnable(t *testing.T) { 134 | if userString != "root" && userString != "system" { 135 | t.Skip("skipping superuser test while running as user") 136 | } 137 | unit := "nginx" 138 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 139 | defer cancel() 140 | err := Mask(ctx, unit, Options{UserMode: false}) 141 | if err != nil { 142 | Unmask(ctx, unit, Options{UserMode: false}) 143 | t.Errorf("Unable to mask %s", unit) 144 | } 145 | err = Enable(ctx, unit, Options{UserMode: false}) 146 | if !errors.Is(err, ErrMasked) { 147 | Unmask(ctx, unit, Options{UserMode: false}) 148 | t.Errorf("error is %v, but should have been %v", err, ErrMasked) 149 | } 150 | err = Unmask(ctx, unit, Options{UserMode: false}) 151 | if err != nil { 152 | t.Errorf("Unable to unmask %s", unit) 153 | } 154 | } 155 | 156 | func ExampleEnable() { 157 | unit := "syncthing" 158 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 159 | defer cancel() 160 | 161 | err := Enable(ctx, unit, Options{UserMode: true}) 162 | switch { 163 | case errors.Is(err, ErrMasked): 164 | fmt.Printf("%s is masked, unmask it before enabling\n", unit) 165 | case errors.Is(err, ErrDoesNotExist): 166 | fmt.Printf("%s does not exist\n", unit) 167 | case errors.Is(err, ErrInsufficientPermissions): 168 | fmt.Printf("permission to enable %s denied\n", unit) 169 | case errors.Is(err, ErrBusFailure): 170 | fmt.Printf("Cannot communicate with the bus\n") 171 | case err == nil: 172 | fmt.Printf("%s enabled successfully\n", unit) 173 | default: 174 | fmt.Printf("Error: %v", err) 175 | } 176 | } 177 | 178 | func TestIsActive(t *testing.T) { 179 | unit := "nginx" 180 | t.Run("check active", func(t *testing.T) { 181 | if testing.Short() { 182 | t.Skip("skipping in short mode") 183 | } 184 | if userString != "root" && userString != "system" { 185 | t.Skip("skipping superuser test while running as user") 186 | } 187 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 188 | defer cancel() 189 | err := Restart(ctx, unit, Options{UserMode: false}) 190 | if err != nil { 191 | t.Errorf("Unable to restart %s: %v", unit, err) 192 | } 193 | time.Sleep(time.Second) 194 | isActive, err := IsActive(ctx, unit, Options{UserMode: false}) 195 | if !isActive { 196 | t.Errorf("IsActive didn't return true for %s: %v", unit, err) 197 | } 198 | }) 199 | t.Run("check masked", func(t *testing.T) { 200 | if userString != "root" && userString != "system" { 201 | t.Skip("skipping superuser test while running as user") 202 | } 203 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 204 | defer cancel() 205 | err := Mask(ctx, unit, Options{UserMode: false}) 206 | if err != nil { 207 | t.Errorf("Unable to mask %s", unit) 208 | } 209 | _, err = IsActive(ctx, unit, Options{UserMode: false}) 210 | if err != nil { 211 | t.Errorf("error is %v, but should have been %v", err, nil) 212 | } 213 | Unmask(ctx, unit, Options{UserMode: false}) 214 | }) 215 | t.Run("check masked", func(t *testing.T) { 216 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 217 | defer cancel() 218 | _, err := IsActive(ctx, "nonexistant", Options{UserMode: false}) 219 | if err != nil { 220 | t.Errorf("error is %v, but should have been %v", err, ErrDoesNotExist) 221 | } 222 | }) 223 | } 224 | 225 | func TestIsEnabled(t *testing.T) { 226 | unit := "nginx" 227 | userMode := false 228 | if userString != "root" && userString != "system" { 229 | userMode = true 230 | unit = "syncthing" 231 | } 232 | t.Run("check enabled", func(t *testing.T) { 233 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 234 | defer cancel() 235 | err := Enable(ctx, unit, Options{UserMode: userMode}) 236 | if err != nil { 237 | t.Errorf("Unable to enable %s: %v", unit, err) 238 | } 239 | isEnabled, err := IsEnabled(ctx, unit, Options{UserMode: userMode}) 240 | if !isEnabled { 241 | t.Errorf("IsEnabled didn't return true for %s: %v", unit, err) 242 | } 243 | }) 244 | t.Run("check disabled", func(t *testing.T) { 245 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 246 | defer cancel() 247 | err := Disable(ctx, unit, Options{UserMode: userMode}) 248 | if err != nil { 249 | t.Errorf("Unable to disable %s", unit) 250 | } 251 | isEnabled, err := IsEnabled(ctx, unit, Options{UserMode: userMode}) 252 | if err != nil { 253 | t.Errorf("Error: %v", err) 254 | } 255 | if isEnabled { 256 | t.Errorf("IsEnabled didn't return false for %s", unit) 257 | } 258 | Enable(ctx, unit, Options{UserMode: false}) 259 | }) 260 | t.Run("check masked", func(t *testing.T) { 261 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 262 | defer cancel() 263 | err := Mask(ctx, unit, Options{UserMode: userMode}) 264 | if err != nil { 265 | t.Errorf("Unable to mask %s", unit) 266 | } 267 | isEnabled, err := IsEnabled(ctx, unit, Options{UserMode: userMode}) 268 | if err != ErrMasked { 269 | t.Errorf("error is %v, but should have been %v", err, ErrMasked) 270 | } 271 | if isEnabled { 272 | t.Errorf("IsEnabled didn't return false for %s", unit) 273 | } 274 | Unmask(ctx, unit, Options{UserMode: userMode}) 275 | Enable(ctx, unit, Options{UserMode: userMode}) 276 | }) 277 | } 278 | 279 | func TestMask(t *testing.T) { 280 | errCases := []struct { 281 | unit string 282 | err error 283 | opts Options 284 | runAsUser bool 285 | }{ 286 | /* Run these tests only as an unpriviledged user */ 287 | 288 | // try nonexistant unit in user mode as user 289 | {"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true}, 290 | // try existing unit in user mode as user 291 | {"syncthing", nil, Options{UserMode: true}, true}, 292 | // try nonexisting unit in system mode as user 293 | {"nonexistant", ErrDoesNotExist, Options{UserMode: false}, true}, 294 | // try existing unit in system mode as user 295 | {"nginx", ErrInsufficientPermissions, Options{UserMode: false}, true}, 296 | 297 | /* End user tests*/ 298 | 299 | /* Run these tests only as a superuser */ 300 | 301 | // try nonexistant unit in system mode as system 302 | {"nonexistant", ErrDoesNotExist, Options{UserMode: false}, false}, 303 | // try existing unit in system mode as system 304 | {"nginx", ErrBusFailure, Options{UserMode: true}, false}, 305 | // try existing unit in system mode as system 306 | {"nginx", nil, Options{UserMode: false}, false}, 307 | 308 | /* End superuser tests*/ 309 | 310 | } 311 | for _, tc := range errCases { 312 | t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) { 313 | if (userString == "root" || userString == "system") && tc.runAsUser { 314 | t.Skip("skipping user test while running as superuser") 315 | } else if (userString != "root" && userString != "system") && !tc.runAsUser { 316 | t.Skip("skipping superuser test while running as user") 317 | } 318 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 319 | defer cancel() 320 | err := Mask(ctx, tc.unit, tc.opts) 321 | if !errors.Is(err, tc.err) { 322 | t.Errorf("error is %v, but should have been %v", err, tc.err) 323 | } 324 | Unmask(ctx, tc.unit, tc.opts) 325 | }) 326 | } 327 | t.Run("test double masking existing", func(t *testing.T) { 328 | unit := "nginx" 329 | userMode := false 330 | if userString != "root" && userString != "system" { 331 | userMode = true 332 | unit = "syncthing" 333 | } 334 | opts := Options{UserMode: userMode} 335 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 336 | defer cancel() 337 | err := Mask(ctx, unit, opts) 338 | if err != nil { 339 | t.Errorf("error on initial masking is %v, but should have been %v", err, nil) 340 | } 341 | err = Mask(ctx, unit, opts) 342 | if err != nil { 343 | t.Errorf("error on second masking is %v, but should have been %v", err, nil) 344 | } 345 | Unmask(ctx, unit, opts) 346 | }) 347 | t.Run("test double masking nonexisting", func(t *testing.T) { 348 | unit := "nonexistant" 349 | userMode := userString != "root" && userString != "system" 350 | 351 | opts := Options{UserMode: userMode} 352 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 353 | defer cancel() 354 | err := Mask(ctx, unit, opts) 355 | if !errors.Is(err, ErrDoesNotExist) { 356 | t.Errorf("error on initial masking is %v, but should have been %v", err, ErrDoesNotExist) 357 | } 358 | err = Mask(ctx, unit, opts) 359 | if err != nil { 360 | t.Errorf("error on second masking is %v, but should have been %v", err, nil) 361 | } 362 | Unmask(ctx, unit, opts) 363 | }) 364 | } 365 | 366 | func TestRestart(t *testing.T) { 367 | unit := "nginx" 368 | userMode := false 369 | if userString != "root" && userString != "system" { 370 | userMode = true 371 | unit = "syncthing" 372 | } 373 | opts := Options{UserMode: userMode} 374 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 375 | defer cancel() 376 | restarts, err := GetNumRestarts(ctx, unit, opts) 377 | if err != nil { 378 | t.Errorf("issue getting number of restarts for %s: %v", unit, err) 379 | } 380 | Start(ctx, unit, opts) 381 | pid, err := GetPID(ctx, unit, opts) 382 | if err != nil { 383 | t.Errorf("issue getting MainPID for %s as %s: %v", unit, userString, err) 384 | } 385 | syscall.Kill(pid, syscall.SIGKILL) 386 | for { 387 | running, errIsActive := IsActive(ctx, unit, opts) 388 | if errIsActive != nil { 389 | t.Errorf("error asserting %s is up: %v", unit, errIsActive) 390 | break 391 | } else if running { 392 | break 393 | } 394 | } 395 | secondRestarts, err := GetNumRestarts(ctx, unit, opts) 396 | if err != nil { 397 | t.Errorf("issue getting second reading on number of restarts for %s: %v", unit, err) 398 | } 399 | if restarts+1 != secondRestarts { 400 | t.Errorf("Expected restart count to differ by one, but difference was: %d", secondRestarts-restarts) 401 | } 402 | } 403 | 404 | // Runs through all defined Properties in parallel and checks for error cases 405 | func TestShow(t *testing.T) { 406 | if testing.Short() { 407 | t.Skip("skipping test in short mode.") 408 | } 409 | unit := "nginx" 410 | opts := Options{ 411 | UserMode: false, 412 | } 413 | for _, x := range properties.Properties { 414 | func(x properties.Property) { 415 | t.Run(fmt.Sprintf("show property %s", string(x)), func(t *testing.T) { 416 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 417 | defer cancel() 418 | t.Parallel() 419 | _, err := Show(ctx, unit, x, opts) 420 | if err != nil { 421 | t.Errorf("error is %v, but should have been %v", err, nil) 422 | } 423 | }) 424 | }(x) 425 | } 426 | } 427 | 428 | func TestStart(t *testing.T) { 429 | unit := "nginx" 430 | userMode := false 431 | if userString != "root" && userString != "system" { 432 | userMode = true 433 | unit = "syncthing" 434 | } 435 | opts := Options{UserMode: userMode} 436 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 437 | defer cancel() 438 | Stop(ctx, unit, opts) 439 | for { 440 | running, err := IsActive(ctx, unit, opts) 441 | if err != nil { 442 | t.Errorf("error asserting %s is up: %v", unit, err) 443 | break 444 | } else if !running { 445 | break 446 | } 447 | } 448 | err := Start(ctx, unit, opts) 449 | if err != nil { 450 | t.Errorf("error: %v", err) 451 | } 452 | for { 453 | running, err := IsActive(ctx, unit, opts) 454 | if err != nil { 455 | t.Errorf("error asserting %s started: %v", unit, err) 456 | break 457 | } else if running { 458 | break 459 | } 460 | } 461 | } 462 | 463 | func TestStatus(t *testing.T) { 464 | unit := "nginx" 465 | userMode := false 466 | opts := Options{UserMode: userMode} 467 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 468 | defer cancel() 469 | _, err := Status(ctx, unit, opts) 470 | if err != nil { 471 | t.Errorf("error: %v", err) 472 | } 473 | } 474 | 475 | func TestStop(t *testing.T) { 476 | unit := "nginx" 477 | userMode := false 478 | if userString != "root" && userString != "system" { 479 | userMode = true 480 | unit = "syncthing" 481 | } 482 | opts := Options{UserMode: userMode} 483 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 484 | defer cancel() 485 | Start(ctx, unit, opts) 486 | for { 487 | running, err := IsActive(ctx, unit, opts) 488 | if err != nil { 489 | t.Errorf("error asserting %s is up: %v", unit, err) 490 | break 491 | } else if running { 492 | break 493 | } 494 | } 495 | err := Stop(ctx, unit, opts) 496 | if err != nil { 497 | t.Errorf("error: %v", err) 498 | } 499 | for { 500 | running, err := IsActive(ctx, unit, opts) 501 | if err != nil { 502 | t.Errorf("error asserting %s stopped: %v", unit, err) 503 | break 504 | } else if !running { 505 | break 506 | } 507 | } 508 | } 509 | 510 | func TestUnmask(t *testing.T) { 511 | errCases := []struct { 512 | unit string 513 | err error 514 | opts Options 515 | runAsUser bool 516 | }{ 517 | /* Run these tests only as an unpriviledged user */ 518 | 519 | // try nonexistant unit in user mode as user 520 | {"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true}, 521 | // try existing unit in user mode as user 522 | {"syncthing", nil, Options{UserMode: true}, true}, 523 | // try nonexisting unit in system mode as user 524 | {"nonexistant", ErrDoesNotExist, Options{UserMode: false}, true}, 525 | // try existing unit in system mode as user 526 | {"nginx", ErrInsufficientPermissions, Options{UserMode: false}, true}, 527 | 528 | /* End user tests*/ 529 | 530 | /* Run these tests only as a superuser */ 531 | 532 | // try nonexistant unit in system mode as system 533 | {"nonexistant", ErrDoesNotExist, Options{UserMode: false}, false}, 534 | // try existing unit in system mode as system 535 | {"nginx", ErrBusFailure, Options{UserMode: true}, false}, 536 | // try existing unit in system mode as system 537 | {"nginx", nil, Options{UserMode: false}, false}, 538 | 539 | /* End superuser tests*/ 540 | 541 | } 542 | for _, tc := range errCases { 543 | t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) { 544 | if (userString == "root" || userString == "system") && tc.runAsUser { 545 | t.Skip("skipping user test while running as superuser") 546 | } else if (userString != "root" && userString != "system") && !tc.runAsUser { 547 | t.Skip("skipping superuser test while running as user") 548 | } 549 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 550 | defer cancel() 551 | err := Mask(ctx, tc.unit, tc.opts) 552 | if !errors.Is(err, tc.err) { 553 | t.Errorf("error is %v, but should have been %v", err, tc.err) 554 | } 555 | Unmask(ctx, tc.unit, tc.opts) 556 | }) 557 | } 558 | t.Run("test double unmasking existing", func(t *testing.T) { 559 | unit := "nginx" 560 | userMode := false 561 | if userString != "root" && userString != "system" { 562 | userMode = true 563 | unit = "syncthing" 564 | } 565 | opts := Options{UserMode: userMode} 566 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 567 | defer cancel() 568 | err := Unmask(ctx, unit, opts) 569 | if err != nil { 570 | t.Errorf("error on initial unmasking is %v, but should have been %v", err, nil) 571 | } 572 | err = Mask(ctx, unit, opts) 573 | if err != nil { 574 | t.Errorf("error on second unmasking is %v, but should have been %v", err, nil) 575 | } 576 | Unmask(ctx, unit, opts) 577 | }) 578 | t.Run("test double unmasking nonexisting", func(t *testing.T) { 579 | unit := "nonexistant" 580 | userMode := userString != "root" && userString != "system" 581 | 582 | opts := Options{UserMode: userMode} 583 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 584 | defer cancel() 585 | Mask(ctx, unit, opts) 586 | err := Unmask(ctx, unit, opts) 587 | if err != nil { 588 | t.Errorf("error on initial unmasking is %v, but should have been %v", err, nil) 589 | } 590 | err = Unmask(ctx, unit, opts) 591 | if !errors.Is(err, ErrDoesNotExist) { 592 | t.Errorf("error on second unmasking is %v, but should have been %v", err, ErrDoesNotExist) 593 | } 594 | }) 595 | } 596 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package systemctl 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "errors" 9 | "fmt" 10 | "os/exec" 11 | "strings" 12 | ) 13 | 14 | var systemctl string 15 | 16 | const killed = 130 17 | 18 | func init() { 19 | path, _ := exec.LookPath("systemctl") 20 | systemctl = path 21 | } 22 | 23 | func execute(ctx context.Context, args []string) (string, string, int, error) { 24 | var ( 25 | err error 26 | stderr bytes.Buffer 27 | stdout bytes.Buffer 28 | code int 29 | output string 30 | warnings string 31 | ) 32 | 33 | if systemctl == "" { 34 | return "", "", 1, ErrNotInstalled 35 | } 36 | cmd := exec.CommandContext(ctx, systemctl, args...) 37 | cmd.Stdout = &stdout 38 | cmd.Stderr = &stderr 39 | err = cmd.Run() 40 | output = stdout.String() 41 | warnings = stderr.String() 42 | code = cmd.ProcessState.ExitCode() 43 | 44 | customErr := filterErr(warnings) 45 | if customErr != nil { 46 | err = customErr 47 | } 48 | if code != 0 && err == nil { 49 | err = fmt.Errorf("received error code %d for stderr `%s`: %w", code, warnings, ErrUnspecified) 50 | } 51 | 52 | return output, warnings, code, err 53 | } 54 | 55 | func filterErr(stderr string) error { 56 | switch { 57 | case strings.Contains(stderr, `does not exist`): 58 | return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr)) 59 | case strings.Contains(stderr, `not found.`): 60 | return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr)) 61 | case strings.Contains(stderr, `not loaded.`): 62 | return errors.Join(ErrUnitNotLoaded, fmt.Errorf("stderr: %s", stderr)) 63 | case strings.Contains(stderr, `No such file or directory`): 64 | return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr)) 65 | case strings.Contains(stderr, `Interactive authentication required`): 66 | return errors.Join(ErrInsufficientPermissions, fmt.Errorf("stderr: %s", stderr)) 67 | case strings.Contains(stderr, `Access denied`): 68 | return errors.Join(ErrInsufficientPermissions, fmt.Errorf("stderr: %s", stderr)) 69 | case strings.Contains(stderr, `DBUS_SESSION_BUS_ADDRESS`): 70 | return errors.Join(ErrBusFailure, fmt.Errorf("stderr: %s", stderr)) 71 | case strings.Contains(stderr, `is masked`): 72 | return errors.Join(ErrMasked, fmt.Errorf("stderr: %s", stderr)) 73 | case strings.Contains(stderr, `Failed`): 74 | return errors.Join(ErrUnspecified, fmt.Errorf("stderr: %s", stderr)) 75 | default: 76 | return nil 77 | } 78 | } 79 | --------------------------------------------------------------------------------