Skip to content

Commit 2da0b29

Browse files
committed
Sanitize systemd user services
In some cases, snapd insists on enabling user services not only globally, as expected (thus, adding a symlink at `/etc/systemd/user/XXXXX.target.wants`), but also locally (thus, at $HOME/.config/systemd/user/XXXX.target.wants`). This means that if an user service is migrated from `default.target` to `graphical-session.target` (or vice-versa), the soft link at the user's HOME folder will be in the wrong subfolder. Unfortunately, the main snapd service seems to not have the capability of accessing and managing the local user services, having to rely on the user's local daemon (`snap userd`), which runs as an user's service. So a change in that daemon (which has its source code at `usersession/userd` folder) is required to check the locally enabled user services and ensure that their soft link is in the right place. This commit does this by checking the links in the user folder every time user session daemon is launched, and runs as the user a `systemctl --user reenable SNAP-SERVICE-NAME` to rebuild the wrong softlink.
1 parent 6e4eb95 commit 2da0b29

File tree

1 file changed

+95
-0
lines changed

1 file changed

+95
-0
lines changed

usersession/userd/userd.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,20 @@ package userd
2121

2222
import (
2323
"fmt"
24+
"io/fs"
25+
"os"
26+
"path"
27+
"path/filepath"
28+
"strings"
2429

2530
"github.com/godbus/dbus/v5"
2631
"github.com/godbus/dbus/v5/introspect"
32+
"gopkg.in/ini.v1"
2733
"gopkg.in/tomb.v2"
2834

2935
"github.com/snapcore/snapd/dbusutil"
3036
"github.com/snapcore/snapd/logger"
37+
"github.com/snapcore/snapd/systemd"
3138
)
3239

3340
type dbusInterface interface {
@@ -50,6 +57,92 @@ var userdBusNames = []string{
5057
"io.snapcraft.Settings",
5158
}
5259

60+
func clearBrokenLink(targetPath string) error {
61+
_, err := filepath.EvalSymlinks(targetPath)
62+
if err != nil {
63+
switch err.(type) {
64+
case *fs.PathError:
65+
logger.Noticef("deleting broken link in snap service %s", targetPath)
66+
// it's a broken link; delete it
67+
os.Remove(targetPath)
68+
default:
69+
return err
70+
}
71+
}
72+
return nil
73+
}
74+
75+
func reenableUserService(serviceName string) error {
76+
logger.Noticef("re-enabling user service %s", serviceName)
77+
sysd := systemd.New(systemd.UserMode, nil)
78+
return sysd.DaemonReEnable([]string{serviceName})
79+
}
80+
81+
func checkServicePlacement(targetName, snapTarget string) error {
82+
snapTargetData, err := os.ReadFile(snapTarget)
83+
if err != nil {
84+
return err
85+
}
86+
targetIni, err := ini.Load(snapTargetData)
87+
if err != nil {
88+
return err
89+
}
90+
wantedBy := ""
91+
if section := targetIni.Section("Install"); section != nil {
92+
wantedBy = section.Key("WantedBy").String()
93+
}
94+
if wantedBy != targetName {
95+
// the symlink is in the wrong folder. Re-enable the service
96+
// to change it.
97+
err := reenableUserService(path.Base(snapTarget))
98+
if err != nil {
99+
return err
100+
}
101+
}
102+
return nil
103+
}
104+
105+
func sanitizeUserServices() error {
106+
// ensure that the user services enabled in the $HOME folder are
107+
// correctly placed in the right .target.wants folder. This placement
108+
// will be wrong if a service is moved from default.target to
109+
// graphical-session.target or vice-versa, so this clean up is required.
110+
//
111+
// The change can happen in two cases:
112+
//
113+
// * when migrating from an old version of snapd without "graphical-session.target"
114+
// support, to a new version that supports it: all the user daemons' WantedBy entry
115+
// will be updated, and so these entries will have to be updated too.
116+
// * when a snap adds or removes the `desktop` plug in a daemon
117+
118+
userSystemdQuery := path.Join(os.Getenv("HOME"), ".config", "systemd", "user", "*.target.wants")
119+
entries, err := filepath.Glob(userSystemdQuery)
120+
if err != nil {
121+
return err
122+
}
123+
for _, targetWantPath := range entries {
124+
snapTargetPaths, err := filepath.Glob(path.Join(targetWantPath, "snap.*"))
125+
if err != nil {
126+
logger.Noticef("cannot get the user service files at %s: %s", targetWantPath, err.Error())
127+
continue
128+
}
129+
for _, snapTarget := range snapTargetPaths {
130+
// Remove any broken link (that's a service that was removed)
131+
if err = clearBrokenLink(snapTarget); err != nil {
132+
logger.Noticef("cannot remove the broken link %s: %s", snapTarget, err.Error())
133+
continue
134+
}
135+
// Check that any snap service is in the right .target.wants
136+
targetName := strings.TrimSuffix(targetWantPath, ".wants")
137+
if err := checkServicePlacement(path.Base(targetName), snapTarget); err != nil {
138+
logger.Noticef("cannot process user service at %s for %s: %s", snapTarget, targetName, err.Error())
139+
continue
140+
}
141+
}
142+
}
143+
return nil
144+
}
145+
53146
func (ud *Userd) Init() error {
54147
var err error
55148

@@ -86,6 +179,8 @@ func (ud *Userd) Init() error {
86179
return fmt.Errorf("cannot obtain bus name '%s'", name)
87180
}
88181
}
182+
183+
sanitizeUserServices()
89184
return nil
90185
}
91186

0 commit comments

Comments
 (0)