Skip to content

Commit

Permalink
cmd/snap: user session application autostart v3 (#4942)
Browse files Browse the repository at this point in the history
Add support for `snap userd --autostart`. The command is expected to be run as part of user session startup and is responsible for starting snap applications that have placed a proper `*.desktop` file in `$SNAP_USER_DATA/.config/autostart`. The desktop file is matched against `autostart` declaration in snap's YAML.

For details see https://forum.snapcraft.io/t/development-sprint-march-5th-2018/4345/2

Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>
  • Loading branch information
bboozzoo authored Apr 11, 2018
1 parent 7cf21a8 commit 98db4bc
Show file tree
Hide file tree
Showing 18 changed files with 654 additions and 26 deletions.
26 changes: 7 additions & 19 deletions cmd/snap/cmd_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,28 +237,16 @@ func antialias(snapApp string, args []string) (string, []string) {
return actualApp, argsOut
}

func getSnapInfo(snapName string, revision snap.Revision) (*snap.Info, error) {
func getSnapInfo(snapName string, revision snap.Revision) (info *snap.Info, err error) {
if revision.Unset() {
curFn := filepath.Join(dirs.SnapMountDir, snapName, "current")
realFn, err := os.Readlink(curFn)
if err != nil {
return nil, fmt.Errorf("cannot find current revision for snap %s: %s", snapName, err)
}
rev := filepath.Base(realFn)
revision, err = snap.ParseRevision(rev)
if err != nil {
return nil, fmt.Errorf("cannot read revision %s: %s", rev, err)
}
}

info, err := snap.ReadInfo(snapName, &snap.SideInfo{
Revision: revision,
})
if err != nil {
return nil, err
info, err = snap.ReadCurrentInfo(snapName)
} else {
info, err = snap.ReadInfo(snapName, &snap.SideInfo{
Revision: revision,
})
}

return info, nil
return info, err
}

func createOrUpdateUserDataSymlink(info *snap.Info, usr *user.User) error {
Expand Down
20 changes: 16 additions & 4 deletions cmd/snap/cmd_userd.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import (

type cmdUserd struct {
userd userd.Userd

Autostart bool `long:"autostart"`
}

var shortUserdHelp = i18n.G("Start the userd service")
Expand All @@ -46,10 +48,9 @@ func init() {
longUserdHelp,
func() flags.Commander {
return &cmdUserd{}
},
nil,
[]argDesc{},
)
}, map[string]string{
"autostart": i18n.G("Autostart user applications"),
}, nil)
cmd.hidden = true
}

Expand All @@ -58,6 +59,10 @@ func (x *cmdUserd) Execute(args []string) error {
return ErrExtraArgs
}

if x.Autostart {
return x.runAutostart()
}

if err := x.userd.Init(); err != nil {
return err
}
Expand All @@ -74,3 +79,10 @@ func (x *cmdUserd) Execute(args []string) error {

return x.userd.Stop()
}

func (x *cmdUserd) runAutostart() error {
if err := userd.AutostartSessionApps(); err != nil {
return fmt.Errorf("autostart failed for the following apps:\n%v", err)
}
return nil
}
1 change: 1 addition & 0 deletions data/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ all install clean:
$(MAKE) -C systemd $@
$(MAKE) -C dbus $@
$(MAKE) -C env $@
$(MAKE) -C desktop $@
38 changes: 38 additions & 0 deletions data/desktop/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#
# Copyright (C) 2018 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

BINDIR = /usr/bin
SYSCONFXDGAUTOSTARTDIR = /etc/xdg/autostart

SOURCES = snap-userd-autostart.desktop.in
DESKTOP_FILES = $(SOURCES:.in=)

.PHONY: all
all: $(DESKTOP_FILES)

.PHONY: install
install: $(DESKTOP_FILES)
# NOTE: old (e.g. 14.04) GNU coreutils doesn't -D with -t
install -d -m 0755 $(DESTDIR)/$(SYSCONFXDGAUTOSTARTDIR)
install -m 0644 -t $(DESTDIR)/$(SYSCONFXDGAUTOSTARTDIR) $^

.PHONY: clean
clean:
rm -f $(DESKTOP_FILES)

%: %.in
cat $< | \
sed s:@bindir@:$(BINDIR):g | \
cat > $@
5 changes: 5 additions & 0 deletions data/desktop/snap-userd-autostart.desktop.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[Desktop Entry]
Name=Snap user application autostart helper
Comment=Helper program for launching snap applications that are configured to start automatically.
Exec=@bindir@/snap userd --autostart
Type=Application
5 changes: 4 additions & 1 deletion dirs/dirs.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ const (
// are in the snap confinement environment.
CoreLibExecDir = "/usr/lib/snapd"
CoreSnapMountDir = "/snap"

// Directory with snap data inside user's home
UserHomeSnapDir = "snap"
)

var (
Expand Down Expand Up @@ -179,7 +182,7 @@ func SetRootDir(rootdir string) {
}

SnapDataDir = filepath.Join(rootdir, "/var/snap")
SnapDataHomeGlob = filepath.Join(rootdir, "/home/*/snap/")
SnapDataHomeGlob = filepath.Join(rootdir, "/home/*/", UserHomeSnapDir)
SnapAppArmorDir = filepath.Join(rootdir, snappyDir, "apparmor", "profiles")
SnapConfineAppArmorDir = filepath.Join(rootdir, snappyDir, "apparmor", "snap-confine")
AppArmorCacheDir = filepath.Join(rootdir, "/var/cache/apparmor")
Expand Down
1 change: 1 addition & 0 deletions packaging/fedora/snapd.spec
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ popd
%{_datadir}/dbus-1/services/io.snapcraft.Launcher.service
%{_datadir}/dbus-1/services/io.snapcraft.Settings.service
%{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy
%{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop
%config(noreplace) %{_sysconfdir}/sysconfig/snapd
%dir %{_sharedstatedir}/snapd
%dir %{_sharedstatedir}/snapd/assertions
Expand Down
1 change: 1 addition & 0 deletions packaging/opensuse-42.2/snapd.spec
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ fi
%{_mandir}/man1/snap.1.gz
/usr/share/dbus-1/services/io.snapcraft.Launcher.service
/usr/share/dbus-1/services/io.snapcraft.Settings.service
%{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop

%changelog

20 changes: 18 additions & 2 deletions snap/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,12 +304,12 @@ func (s *Info) DataDir() string {

// UserDataDir returns the user-specific data directory of the snap.
func (s *Info) UserDataDir(home string) string {
return filepath.Join(home, "snap", s.Name(), s.Revision.String())
return filepath.Join(home, dirs.UserHomeSnapDir, s.Name(), s.Revision.String())
}

// UserCommonDataDir returns the user-specific data directory common across revision of the snap.
func (s *Info) UserCommonDataDir(home string) string {
return filepath.Join(home, "snap", s.Name(), "common")
return filepath.Join(home, dirs.UserHomeSnapDir, s.Name(), "common")
}

// CommonDataDir returns the data directory common across revisions of the snap.
Expand Down Expand Up @@ -780,6 +780,22 @@ func ReadInfo(name string, si *SideInfo) (*Info, error) {
return info, nil
}

// ReadCurrentInfo reads the snap information from the installed snap in 'current' revision
func ReadCurrentInfo(snapName string) (*Info, error) {
curFn := filepath.Join(dirs.SnapMountDir, snapName, "current")
realFn, err := os.Readlink(curFn)
if err != nil {
return nil, fmt.Errorf("cannot find current revision for snap %s: %s", snapName, err)
}
rev := filepath.Base(realFn)
revision, err := ParseRevision(rev)
if err != nil {
return nil, fmt.Errorf("cannot read revision %s: %s", rev, err)
}

return ReadInfo(snapName, &SideInfo{Revision: revision})
}

// ReadInfoFromSnapFile reads the snap information from the given File
// and completes it with the given side-info if this is not nil.
func ReadInfoFromSnapFile(snapf Container, si *SideInfo) (*Info, error) {
Expand Down
17 changes: 17 additions & 0 deletions snap/info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,23 @@ func (s *infoSuite) TestReadInfo(c *C) {
c.Check(snapInfo2, DeepEquals, snapInfo1)
}

func (s *infoSuite) TestReadCurrentInfo(c *C) {
si := &snap.SideInfo{Revision: snap.R(42)}

snapInfo1 := snaptest.MockSnapCurrent(c, sampleYaml, si)

snapInfo2, err := snap.ReadCurrentInfo("sample")
c.Assert(err, IsNil)

c.Check(snapInfo2.Name(), Equals, "sample")
c.Check(snapInfo2.Revision, Equals, snap.R(42))
c.Check(snapInfo2, DeepEquals, snapInfo1)

snapInfo3, err := snap.ReadCurrentInfo("not-sample")
c.Check(snapInfo3, IsNil)
c.Assert(err, ErrorMatches, `cannot find current revision for snap not-sample:.*`)
}

func (s *infoSuite) TestInstallDate(c *C) {
si := &snap.SideInfo{Revision: snap.R(1)}
info := snaptest.MockSnap(c, sampleYaml, si)
Expand Down
12 changes: 12 additions & 0 deletions snap/snaptest/snaptest.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@ func MockSnap(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info {
return snapInfo
}

// MockSnapCurrent does the same as MockSnap but additionally creates the
// 'current' symlink.
//
// The caller is responsible for mocking root directory with dirs.SetRootDir()
// and for altering the overlord state if required.
func MockSnapCurrent(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info {
si := MockSnap(c, yamlText, sideInfo)
err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current"))
c.Assert(err, check.IsNil)
return si
}

// MockInfo parses the given snap.yaml text and returns a validated snap.Info object including the optional SideInfo.
//
// The result is just kept in memory, there is nothing kept on disk. If that is
Expand Down
14 changes: 14 additions & 0 deletions snap/snaptest/snaptest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,20 @@ func (s *snapTestSuite) TestMockSnap(c *C) {
c.Check(snapInfo.Plugs["network"].Interface, Equals, "network")
}

func (s *snapTestSuite) TestMockSnapCurrent(c *C) {
snapInfo := snaptest.MockSnapCurrent(c, sampleYaml, &snap.SideInfo{Revision: snap.R(42)})
// Data from YAML is used
c.Check(snapInfo.Name(), Equals, "sample")
// Data from SideInfo is used
c.Check(snapInfo.Revision, Equals, snap.R(42))
// The YAML is placed on disk
c.Check(filepath.Join(dirs.SnapMountDir, "sample", "42", "meta", "snap.yaml"),
testutil.FileEquals, sampleYaml)
link, err := os.Readlink(filepath.Join(dirs.SnapMountDir, "sample", "current"))
c.Check(err, IsNil)
c.Check(link, Equals, filepath.Join(dirs.SnapMountDir, "sample", "42"))
}

func (s *snapTestSuite) TestMockInfo(c *C) {
snapInfo := snaptest.MockInfo(c, sampleYaml, &snap.SideInfo{Revision: snap.R(42)})
// Data from YAML is used
Expand Down
26 changes: 26 additions & 0 deletions tests/lib/snaps/test-snapd-xdg-autostart/bin/foobar
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/bin/sh

dump_autostart() {
dfpath="$SNAP_USER_DATA/.config/autostart/foo.desktop"

test -e "$dfpath" && return

echo "generating autostart file $dfpath"

mkdir -p $SNAP_USER_DATA/.config/autostart
cat <<EOF > $dfpath
[Desktop Entry]
Name=foo autostart
Exec=/foo/bar/baz/bin/foobar --autostart a b c
EOF
}

case "$1" in
--autostart)
echo "autostarting with args '$@'" | tee $SNAP_USER_DATA/foo-autostarted
;;
*)
echo "regular run with args '$@'"
dump_autostart
;;
esac
6 changes: 6 additions & 0 deletions tests/lib/snaps/test-snapd-xdg-autostart/meta/snap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: test-snapd-xdg-autostart
version: 1.0
apps:
foo:
command: bin/foobar
autostart: foo.desktop
29 changes: 29 additions & 0 deletions tests/main/snap-userd-desktop-app-autostart/task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
summary: Check that snap userd can autostart user session applications

execute: |
echo "When the snap is installed"
. $TESTSLIB/snaps.sh
install_local test-snapd-xdg-autostart
# run the app directly, it will dump a *.desktop file
snap run test-snapd-xdg-autostart.foo
echo "And snap application autostart file exists"
test -e ~/snap/test-snapd-xdg-autostart/current/.config/autostart/foo.desktop
echo "Applications can be automatically started by snap userd --autostart"
test ! -e ~/snap/test-xdg-snap-autostart/current/foo-autostarted
snap userd --autostart > autostart.log 2>&1
# when app is autostarted it dumps a file at $SNAP_USER_DATA/foo-autostarted,
# applications are forked by userd, but userd does not wait for them
for i in `seq 20`; do
test -e ~/snap/test-snapd-xdg-autostart/current/foo-autostarted && break
sleep 1
done
test -e ~/snap/test-snapd-xdg-autostart/current/foo-autostarted
restore: |
rm -f ~/snap/test-snapd-xdg-autostart/current/foo-autostarted
rm -f ~/snap/test-snapd-xdg-autostart/current/.config/autostart/foo.desktop
Loading

0 comments on commit 98db4bc

Please sign in to comment.