Skip to content

Commit

Permalink
o/servicestate: support for user services (canonical#13380)
Browse files Browse the repository at this point in the history
* o/servicestate: support for user services

* o/servicestate: update docstring for easier readability
  • Loading branch information
Meulengracht authored Apr 2, 2024
1 parent 853d600 commit 1bad859
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 33 deletions.
103 changes: 88 additions & 15 deletions overlord/servicestate/servicestate.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
package servicestate

import (
"context"
"errors"
"fmt"
"io"
"os/user"
"path/filepath"
"sort"
"strconv"
"strings"
"time"

Expand All @@ -40,6 +42,7 @@ import (
"github.com/snapcore/snapd/snap/quota"
"github.com/snapcore/snapd/strutil"
"github.com/snapcore/snapd/systemd"
usc "github.com/snapcore/snapd/usersession/client"
"github.com/snapcore/snapd/wrappers"
)

Expand Down Expand Up @@ -350,9 +353,15 @@ func Control(st *state.State, appInfos []*snap.AppInfo, inst *Instruction, cu *u
type StatusDecorator struct {
sysd systemd.Systemd
globalUserSysd systemd.Systemd
context context.Context
uid string
}

// NewStatusDecorator returns a new StatusDecorator.
//
// Using NewStatusDecorator will only allow for global status of user-services
// as StatusDecorator is designed to contain a single set of results for a single
// user.
func NewStatusDecorator(rep interface {
Notify(string)
}) *StatusDecorator {
Expand All @@ -362,6 +371,20 @@ func NewStatusDecorator(rep interface {
}
}

// NewStatusDecoratorForUid returns a new StatusDecorator, but configured
// for a specific uid. This allows the StatusDecorator to get statuses for
// user-services for a specific user.
func NewStatusDecoratorForUid(rep interface {
Notify(string)
}, context context.Context, uid string) *StatusDecorator {
return &StatusDecorator{
sysd: systemd.New(systemd.SystemMode, rep),
globalUserSysd: systemd.New(systemd.GlobalUserMode, rep),
context: context,
uid: uid,
}
}

func (sd *StatusDecorator) hasEnabledActivator(appInfo *client.AppInfo) bool {
// Just one activator should be enabled in order for the service to be able
// to become enabled. For slot activated services this is always true as we
Expand All @@ -374,6 +397,69 @@ func (sd *StatusDecorator) hasEnabledActivator(appInfo *client.AppInfo) bool {
return false
}

// queryUserServiceStatus returns a list of service-statuses for the configured users.
func (sd *StatusDecorator) queryUserServiceStatus(units []string) ([]*systemd.UnitStatus, error) {
// Avoid any expensive call if there are no user daemons
if len(units) == 0 {
return nil, nil
}

uid, err := strconv.Atoi(sd.uid)
if err != nil {
return nil, err
}

cli := usc.NewForUids(uid)
sts, failures, err := cli.ServiceStatus(sd.context, units)
if err != nil {
return nil, err
}

// Return the first service failure, if any failures were reported
if len(failures[uid]) > 0 {
return nil, fmt.Errorf("cannot retrieve service %q status: %v",
failures[uid][0].Service, failures[uid][0].Error)
}

// Convert the received unit statuses to systemd-unit statuses
var sysdStatuses []*systemd.UnitStatus
for _, sts := range sts[uid] {
sysdStatuses = append(sysdStatuses, sts.SystemdUnitStatus())
}
return sysdStatuses, nil
}

func (sd *StatusDecorator) queryServiceStatus(scope snap.DaemonScope, units []string) ([]*systemd.UnitStatus, error) {
var sts []*systemd.UnitStatus
var err error
switch scope {
case snap.SystemDaemon:
// sysd.Status() makes sure that we get only the units we asked
// for and raises an error otherwise.
sts, err = sd.sysd.Status(units)
case snap.UserDaemon:
// Support the previous behavior of retrieving the global enablement
// status of user services if no uid is configured for this status
// decorator.
if sd.uid != "" {
sts, err = sd.queryUserServiceStatus(units)
} else {
sts, err = sd.globalUserSysd.Status(units)
}
default:
return nil, fmt.Errorf("internal error: unknown daemon-scope %q", scope)
}
if err != nil {
return nil, err
}

// ensure we get the correct unit count, otherwise report an error.
if len(sts) != len(units) {
return nil, fmt.Errorf("expected %d results, got %d", len(units), len(sts))
}
return sts, nil
}

// DecorateWithStatus adds service status information to the given
// client.AppInfo associated with the given snap.AppInfo.
// If the snap is inactive or the app is not service it does nothing.
Expand All @@ -385,15 +471,6 @@ func (sd *StatusDecorator) DecorateWithStatus(appInfo *client.AppInfo, snapApp *
// nothing to do
return nil
}
var sysd systemd.Systemd
switch snapApp.DaemonScope {
case snap.SystemDaemon:
sysd = sd.sysd
case snap.UserDaemon:
sysd = sd.globalUserSysd
default:
return fmt.Errorf("internal error: unknown daemon-scope %q", snapApp.DaemonScope)
}

// collect all services for a single call to systemctl
extra := len(snapApp.Sockets)
Expand All @@ -414,15 +491,11 @@ func (sd *StatusDecorator) DecorateWithStatus(appInfo *client.AppInfo, snapApp *
serviceNames = append(serviceNames, timerUnit)
}

// sysd.Status() makes sure that we get only the units we asked
// for and raises an error otherwise
sts, err := sysd.Status(serviceNames)
sts, err := sd.queryServiceStatus(snapApp.DaemonScope, serviceNames)
if err != nil {
return fmt.Errorf("cannot get status of services of app %q: %v", appInfo.Name, err)
}
if len(sts) != len(serviceNames) {
return fmt.Errorf("cannot get status of services of app %q: expected %d results, got %d", appInfo.Name, len(serviceNames), len(sts))
}

for _, st := range sts {
switch filepath.Ext(st.Name) {
case ".service":
Expand Down
143 changes: 139 additions & 4 deletions overlord/servicestate/servicestate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package servicestate_test

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
Expand All @@ -46,16 +47,41 @@ import (
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/systemd"
"github.com/snapcore/snapd/testutil"
"github.com/snapcore/snapd/usersession/agent"
"github.com/snapcore/snapd/wrappers"
)

type statusDecoratorSuite struct{}
type statusDecoratorSuite struct {
testutil.DBusTest
tempdir string
agent *agent.SessionAgent
}

var _ = Suite(&statusDecoratorSuite{})

func (s *statusDecoratorSuite) SetUpTest(c *C) {
s.DBusTest.SetUpTest(c)
s.tempdir = c.MkDir()
dirs.SetRootDir(s.tempdir)

xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid())
err := os.MkdirAll(xdgRuntimeDir, 0700)
c.Assert(err, IsNil)
s.agent, err = agent.New()
c.Assert(err, IsNil)
s.agent.Start()
}

func (s *statusDecoratorSuite) TearDownTest(c *C) {
if s.agent != nil {
err := s.agent.Stop()
c.Check(err, IsNil)
}
dirs.SetRootDir("")
s.DBusTest.TearDownTest(c)
}

func (s *statusDecoratorSuite) TestDecorateWithStatus(c *C) {
dirs.SetRootDir(c.MkDir())
defer dirs.SetRootDir("")
snp := &snap.Info{
SideInfo: snap.SideInfo{
RealName: "foo",
Expand Down Expand Up @@ -229,7 +255,8 @@ NeedDaemonReload=no
{Name: "org.example.Svc", Type: "dbus", Active: true, Enabled: true},
})

// No state is currently extracted for user daemons
// When using a decorator without any uid provided, the global status is
// fetched, which is only enablement
app = &client.AppInfo{
Snap: snp.InstanceName(),
Name: "svc",
Expand Down Expand Up @@ -276,6 +303,114 @@ NeedDaemonReload=no
}
}

func (s *statusDecoratorSuite) TestUserServiceDecorateWithStatus(c *C) {
snp := &snap.Info{
SideInfo: snap.SideInfo{
RealName: "foo",
Revision: snap.R(1),
},
}
err := os.MkdirAll(snp.MountDir(), 0755)
c.Assert(err, IsNil)
err = os.Symlink(snp.Revision.String(), filepath.Join(filepath.Dir(snp.MountDir()), "current"))
c.Assert(err, IsNil)

disabled := false
r := systemd.MockSystemctl(func(args ...string) (buf []byte, err error) {
c.Check(args[0], Equals, "--user")
c.Check(args[1], Equals, "show")
unit := args[3]

activeState, unitState := "active", "enabled"
if disabled {
activeState = "inactive"
unitState = "disabled"
}

if strings.HasSuffix(unit, ".timer") || strings.HasSuffix(unit, ".socket") || strings.HasSuffix(unit, ".target") {
// Units using the baseProperties query
return []byte(fmt.Sprintf(`Id=%s
Names=%[1]s
ActiveState=%s
UnitFileState=%s
`, unit, activeState, unitState)), nil
} else {
// Units using the extendedProperties query
return []byte(fmt.Sprintf(`Id=%s
Names=%[1]s
Type=simple
ActiveState=%s
UnitFileState=%s
NeedDaemonReload=no
`, unit, activeState, unitState)), nil
}
})
defer r()

curr, err := user.Current()
c.Assert(err, IsNil)

sd := servicestate.NewStatusDecoratorForUid(nil, context.Background(), curr.Uid)

// not a service
app := &client.AppInfo{
Snap: "foo",
Name: "app",
}
snapApp := &snap.AppInfo{Snap: snp, Name: "app"}

err = sd.DecorateWithStatus(app, snapApp)
c.Assert(err, IsNil)

for _, enabled := range []bool{true, false} {
disabled = !enabled

app = &client.AppInfo{
Snap: snp.InstanceName(),
Name: "svc",
Daemon: "simple",
}
snapApp = &snap.AppInfo{
Snap: snp,
Name: "svc",
Daemon: "simple",
DaemonScope: snap.UserDaemon,
}
snapApp.Sockets = map[string]*snap.SocketInfo{
"socket1": {
App: snapApp,
Name: "socket1",
ListenStream: "a.socket",
},
}
snapApp.Timer = &snap.TimerInfo{
App: snapApp,
Timer: "10:00",
}
snapApp.ActivatesOn = []*snap.SlotInfo{
{
Snap: snp,
Name: "dbus-slot",
Interface: "dbus",
Attrs: map[string]interface{}{
"bus": "session",
"name": "org.example.Svc",
},
},
}

err = sd.DecorateWithStatus(app, snapApp)
c.Assert(err, IsNil)
c.Check(app.Active, Equals, enabled)
c.Check(app.Enabled, Equals, true) // when a service is slot activated its always enabled
c.Check(app.Activators, DeepEquals, []client.AppActivator{
{Name: "socket1", Type: "socket", Active: enabled, Enabled: enabled},
{Name: "svc", Type: "timer", Active: enabled, Enabled: enabled},
{Name: "org.example.Svc", Type: "dbus", Active: true, Enabled: true},
})
}
}

type instructionSuite struct {
rootUser *user.User
defaultUser *user.User
Expand Down
14 changes: 14 additions & 0 deletions usersession/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"time"

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/systemd"
)

// dialSessionAgent connects to a user's session agent
Expand Down Expand Up @@ -334,6 +335,19 @@ type ServiceUnitStatus struct {
NeedDaemonReload bool `json:"needs-reload"`
}

func (us *ServiceUnitStatus) SystemdUnitStatus() *systemd.UnitStatus {
return &systemd.UnitStatus{
Daemon: us.Daemon,
Id: us.Id,
Name: us.Name,
Names: us.Names,
Enabled: us.Enabled,
Active: us.Active,
Installed: us.Installed,
NeedDaemonReload: us.NeedDaemonReload,
}
}

func (client *Client) ServiceStatus(ctx context.Context, services []string) (map[int][]ServiceUnitStatus, map[int][]ServiceFailure, error) {
q := make(url.Values)
q.Add("services", strings.Join(services, ","))
Expand Down
15 changes: 1 addition & 14 deletions wrappers/internal/service_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,23 +123,10 @@ var userSessionQueryServiceStatusMany = func(units []string) (map[int][]client.S
return cli.ServiceStatus(ctx, units)
}

func clientUnitStatusToSystemdUnitStatus(unitStatus client.ServiceUnitStatus) *systemd.UnitStatus {
return &systemd.UnitStatus{
Daemon: unitStatus.Daemon,
Id: unitStatus.Id,
Name: unitStatus.Name,
Names: unitStatus.Names,
Enabled: unitStatus.Enabled,
Active: unitStatus.Active,
Installed: unitStatus.Installed,
NeedDaemonReload: unitStatus.NeedDaemonReload,
}
}

func mapServiceStatusMany(stss []client.ServiceUnitStatus) map[string]*systemd.UnitStatus {
stsMap := make(map[string]*systemd.UnitStatus, len(stss))
for _, sts := range stss {
stsMap[sts.Name] = clientUnitStatusToSystemdUnitStatus(sts)
stsMap[sts.Name] = sts.SystemdUnitStatus()
}
return stsMap
}
Expand Down

0 comments on commit 1bad859

Please sign in to comment.