Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,24 @@ jobs:
go-version: ${{ matrix.go-version }}
cache: true

- name: Run tests with race detection and coverage
run: go test -race -coverprofile=coverage.out -covermode=atomic ./...
- name: Install and start nginx
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq nginx
sudo systemctl start nginx
sudo systemctl enable nginx

- name: Run tests (user)
run: go test -race -coverprofile=coverage-user.out -covermode=atomic ./...

- name: Run tests (root)
run: sudo go test -race -coverprofile=coverage-root.out -covermode=atomic ./...

- name: Upload coverage to Codecov
if: matrix.go-version == '1.26'
uses: codecov/codecov-action@v5
with:
files: coverage.out
files: coverage-user.out,coverage-root.out
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false

Expand Down
15 changes: 15 additions & 0 deletions filtererr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ func TestFilterErr(t *testing.T) {
stderr: "Failed to do something unknown",
want: ErrUnspecified,
},
{
name: "does not exist with auth required prioritizes permission error",
stderr: "Unit nginx.service does not exist, proceeding anyway.\nFailed to mask unit: Interactive authentication required.",
want: ErrInsufficientPermissions,
},
{
name: "does not exist with access denied prioritizes permission error",
stderr: "Unit foo.service does not exist, proceeding anyway.\nAccess denied",
want: ErrInsufficientPermissions,
},
{
name: "does not exist with bus failure prioritizes bus error",
stderr: "Unit foo.service does not exist, proceeding anyway.\n$DBUS_SESSION_BUS_ADDRESS not set",
want: ErrBusFailure,
},
{
name: "unrecognized warning",
stderr: "Warning: something benign happened",
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/taigrr/systemctl

go 1.26
go 1.26.1
4 changes: 2 additions & 2 deletions systemctl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ func TestMask(t *testing.T) {
// try existing unit in user mode as user
{"syncthing", nil, Options{UserMode: true}, true},
// try nonexisting unit in system mode as user
{"nonexistant", ErrDoesNotExist, Options{UserMode: false}, true},
{"nonexistant", ErrInsufficientPermissions, Options{UserMode: false}, true},
// try existing unit in system mode as user
{"nginx", ErrInsufficientPermissions, Options{UserMode: false}, true},

Expand Down Expand Up @@ -521,7 +521,7 @@ func TestUnmask(t *testing.T) {
// try existing unit in user mode as user
{"syncthing", nil, Options{UserMode: true}, true},
// try nonexisting unit in system mode as user
{"nonexistant", ErrDoesNotExist, Options{UserMode: false}, true},
{"nonexistant", ErrInsufficientPermissions, Options{UserMode: false}, true},
// try existing unit in system mode as user
{"nginx", ErrInsufficientPermissions, Options{UserMode: false}, true},

Expand Down
22 changes: 14 additions & 8 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,13 @@ func prepareArgs(base string, opts Options, extra ...string) []string {
}

func filterErr(stderr string) error {
// Order matters: check higher-priority errors first.
// For example, `systemctl mask nginx` as a non-root user on a system
// without nginx prints both "does not exist, proceeding anyway" (a
// warning) and "Interactive authentication required" (the real error).
// Permission and bus errors must be checked before "does not exist" so
// the actual failure reason is returned.
switch {
case strings.Contains(stderr, `does not exist`):
return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `not found.`):
return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `not loaded.`):
return errors.Join(ErrUnitNotLoaded, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `No such file or directory`):
return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `Interactive authentication required`):
return errors.Join(ErrInsufficientPermissions, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `Access denied`):
Expand All @@ -87,6 +85,14 @@ func filterErr(stderr string) error {
return errors.Join(ErrBusFailure, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `is masked`):
return errors.Join(ErrMasked, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `does not exist`):
return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `not found.`):
return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `not loaded.`):
return errors.Join(ErrUnitNotLoaded, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `No such file or directory`):
return errors.Join(ErrDoesNotExist, fmt.Errorf("stderr: %s", stderr))
case strings.Contains(stderr, `Failed`):
return errors.Join(ErrUnspecified, fmt.Errorf("stderr: %s", stderr))
default:
Expand Down
Loading