github.com/bugraaydogar/snapd@v0.0.0-20210315170335-8c70bb858939/wrappers/services_gen_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2016 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package wrappers_test
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"os/exec"
    26  	"path/filepath"
    27  	"strings"
    28  	"time"
    29  
    30  	. "gopkg.in/check.v1"
    31  
    32  	"github.com/snapcore/snapd/dirs"
    33  	"github.com/snapcore/snapd/snap"
    34  	"github.com/snapcore/snapd/snap/snaptest"
    35  	"github.com/snapcore/snapd/testutil"
    36  	"github.com/snapcore/snapd/timeout"
    37  	"github.com/snapcore/snapd/timeutil"
    38  	"github.com/snapcore/snapd/wrappers"
    39  )
    40  
    41  type servicesWrapperGenSuite struct {
    42  	testutil.BaseTest
    43  }
    44  
    45  var _ = Suite(&servicesWrapperGenSuite{})
    46  
    47  const expectedServiceFmt = `[Unit]
    48  # Auto-generated, DO NOT EDIT
    49  Description=Service for snap application snap.app
    50  Requires=%s-snap-44.mount
    51  Wants=network.target
    52  After=%s-snap-44.mount network.target snapd.apparmor.service
    53  X-Snappy=yes
    54  
    55  [Service]
    56  EnvironmentFile=-/etc/environment
    57  ExecStart=/usr/bin/snap run snap.app
    58  SyslogIdentifier=snap.app
    59  Restart=%s
    60  WorkingDirectory=/var/snap/snap/44
    61  ExecStop=/usr/bin/snap run --command=stop snap.app
    62  ExecReload=/usr/bin/snap run --command=reload snap.app
    63  ExecStopPost=/usr/bin/snap run --command=post-stop snap.app
    64  TimeoutStopSec=10
    65  Type=%s
    66  %s`
    67  
    68  const expectedInstallSection = `
    69  [Install]
    70  WantedBy=multi-user.target
    71  `
    72  
    73  const expectedUserServiceFmt = `[Unit]
    74  # Auto-generated, DO NOT EDIT
    75  Description=Service for snap application snap.app
    76  X-Snappy=yes
    77  
    78  [Service]
    79  EnvironmentFile=-/etc/environment
    80  ExecStart=/usr/bin/snap run snap.app
    81  SyslogIdentifier=snap.app
    82  Restart=%s
    83  WorkingDirectory=/var/snap/snap/44
    84  ExecStop=/usr/bin/snap run --command=stop snap.app
    85  ExecReload=/usr/bin/snap run --command=reload snap.app
    86  ExecStopPost=/usr/bin/snap run --command=post-stop snap.app
    87  TimeoutStopSec=10
    88  Type=%s
    89  
    90  [Install]
    91  WantedBy=default.target
    92  `
    93  
    94  var (
    95  	mountUnitPrefix = strings.Replace(dirs.SnapMountDir[1:], "/", "-", -1)
    96  )
    97  
    98  var (
    99  	expectedAppService     = fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "on-failure", "simple", expectedInstallSection)
   100  	expectedDbusService    = fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "on-failure", "dbus\nBusName=foo.bar.baz", "")
   101  	expectedOneshotService = fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "no", "oneshot\nRemainAfterExit=yes", expectedInstallSection)
   102  	expectedUserAppService = fmt.Sprintf(expectedUserServiceFmt, "on-failure", "simple")
   103  )
   104  
   105  var (
   106  	expectedServiceWrapperFmt = `[Unit]
   107  # Auto-generated, DO NOT EDIT
   108  Description=Service for snap application xkcd-webserver.xkcd-webserver
   109  Requires=%s-xkcd\x2dwebserver-44.mount
   110  Wants=network.target
   111  After=%s-xkcd\x2dwebserver-44.mount network.target snapd.apparmor.service
   112  X-Snappy=yes
   113  
   114  [Service]
   115  EnvironmentFile=-/etc/environment
   116  ExecStart=/usr/bin/snap run xkcd-webserver
   117  SyslogIdentifier=xkcd-webserver.xkcd-webserver
   118  Restart=on-failure
   119  WorkingDirectory=/var/snap/xkcd-webserver/44
   120  ExecStop=/usr/bin/snap run --command=stop xkcd-webserver
   121  ExecReload=/usr/bin/snap run --command=reload xkcd-webserver
   122  ExecStopPost=/usr/bin/snap run --command=post-stop xkcd-webserver
   123  TimeoutStopSec=30
   124  Type=%s
   125  %s`
   126  	expectedTypeForkingWrapper = fmt.Sprintf(expectedServiceWrapperFmt, mountUnitPrefix, mountUnitPrefix, "forking", expectedInstallSection)
   127  )
   128  
   129  func (s *servicesWrapperGenSuite) SetUpTest(c *C) {
   130  	s.BaseTest.SetUpTest(c)
   131  	s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
   132  }
   133  
   134  func (s *servicesWrapperGenSuite) TearDownTest(c *C) {
   135  	s.BaseTest.TearDownTest(c)
   136  }
   137  
   138  func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFile(c *C) {
   139  	yamlText := `
   140  name: snap
   141  version: 1.0
   142  apps:
   143      app:
   144          command: bin/start
   145          stop-command: bin/stop
   146          reload-command: bin/reload
   147          post-stop-command: bin/stop --post
   148          stop-timeout: 10s
   149          daemon: simple
   150  `
   151  	info, err := snap.InfoFromSnapYaml([]byte(yamlText))
   152  	c.Assert(err, IsNil)
   153  	info.Revision = snap.R(44)
   154  	app := info.Apps["app"]
   155  
   156  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil)
   157  	c.Assert(err, IsNil)
   158  	c.Check(string(generatedWrapper), Equals, expectedAppService)
   159  }
   160  
   161  func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileWithStartTimeout(c *C) {
   162  	yamlText := `
   163  name: snap
   164  version: 1.0
   165  apps:
   166      app:
   167          command: bin/start
   168          start-timeout: 10m
   169          daemon: simple
   170  `
   171  	info, err := snap.InfoFromSnapYaml([]byte(yamlText))
   172  	c.Assert(err, IsNil)
   173  	info.Revision = snap.R(44)
   174  	app := info.Apps["app"]
   175  
   176  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil)
   177  	c.Assert(err, IsNil)
   178  	c.Check(string(generatedWrapper), testutil.Contains, "\nTimeoutStartSec=600\n")
   179  }
   180  
   181  func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileRestart(c *C) {
   182  	yamlTextTemplate := `
   183  name: snap
   184  apps:
   185      app:
   186          daemon: simple
   187          restart-condition: %s
   188  `
   189  	for name, cond := range snap.RestartMap {
   190  		yamlText := fmt.Sprintf(yamlTextTemplate, cond)
   191  
   192  		info, err := snap.InfoFromSnapYaml([]byte(yamlText))
   193  		c.Assert(err, IsNil)
   194  		info.Revision = snap.R(44)
   195  		app := info.Apps["app"]
   196  
   197  		generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil)
   198  		c.Assert(err, IsNil)
   199  		wrapperText := string(generatedWrapper)
   200  		if cond == snap.RestartNever {
   201  			c.Check(wrapperText, Matches,
   202  				`(?ms).*^Restart=no$.*`, Commentf(name))
   203  		} else {
   204  			c.Check(wrapperText, Matches,
   205  				`(?ms).*^Restart=`+name+`$.*`, Commentf(name))
   206  		}
   207  	}
   208  }
   209  
   210  func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileTypeForking(c *C) {
   211  	service := &snap.AppInfo{
   212  		Snap: &snap.Info{
   213  			SuggestedName: "xkcd-webserver",
   214  			Version:       "0.3.4",
   215  			SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   216  		},
   217  		Name:            "xkcd-webserver",
   218  		Command:         "bin/foo start",
   219  		StopCommand:     "bin/foo stop",
   220  		ReloadCommand:   "bin/foo reload",
   221  		PostStopCommand: "bin/foo post-stop",
   222  		StopTimeout:     timeout.DefaultTimeout,
   223  		Daemon:          "forking",
   224  		DaemonScope:     snap.SystemDaemon,
   225  	}
   226  
   227  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(service, nil)
   228  	c.Assert(err, IsNil)
   229  	c.Assert(string(generatedWrapper), Equals, expectedTypeForkingWrapper)
   230  }
   231  
   232  func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileIllegalChars(c *C) {
   233  	service := &snap.AppInfo{
   234  		Snap: &snap.Info{
   235  			SuggestedName: "xkcd-webserver",
   236  			Version:       "0.3.4",
   237  			SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   238  		},
   239  		Name:            "xkcd-webserver",
   240  		Command:         "bin/foo start\n",
   241  		StopCommand:     "bin/foo stop",
   242  		ReloadCommand:   "bin/foo reload",
   243  		PostStopCommand: "bin/foo post-stop",
   244  		StopTimeout:     timeout.DefaultTimeout,
   245  		Daemon:          "simple",
   246  		DaemonScope:     snap.SystemDaemon,
   247  	}
   248  
   249  	_, err := wrappers.GenerateSnapServiceFile(service, nil)
   250  	c.Assert(err, NotNil)
   251  }
   252  
   253  func (s *servicesWrapperGenSuite) TestGenServiceFileWithBusName(c *C) {
   254  	yamlText := `
   255  name: snap
   256  version: 1.0
   257  slots:
   258      dbus-slot:
   259          interface: dbus
   260          bus: system
   261          name: org.example.Foo
   262  apps:
   263      app:
   264          command: bin/start
   265          stop-command: bin/stop
   266          reload-command: bin/reload
   267          post-stop-command: bin/stop --post
   268          stop-timeout: 10s
   269          bus-name: foo.bar.baz
   270          daemon: dbus
   271          activates-on: [dbus-slot]
   272  `
   273  
   274  	info, err := snap.InfoFromSnapYaml([]byte(yamlText))
   275  	c.Assert(err, IsNil)
   276  	info.Revision = snap.R(44)
   277  	app := info.Apps["app"]
   278  
   279  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil)
   280  	c.Assert(err, IsNil)
   281  
   282  	c.Assert(string(generatedWrapper), Equals, expectedDbusService)
   283  }
   284  
   285  func (s *servicesWrapperGenSuite) TestGenServiceFileWithBusNameOnly(c *C) {
   286  
   287  	yamlText := `
   288  name: snap
   289  version: 1.0
   290  apps:
   291      app:
   292          command: bin/start
   293          stop-command: bin/stop
   294          reload-command: bin/reload
   295          post-stop-command: bin/stop --post
   296          stop-timeout: 10s
   297          bus-name: foo.bar.baz
   298          daemon: dbus
   299  `
   300  
   301  	info, err := snap.InfoFromSnapYaml([]byte(yamlText))
   302  	c.Assert(err, IsNil)
   303  	info.Revision = snap.R(44)
   304  	app := info.Apps["app"]
   305  
   306  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil)
   307  	c.Assert(err, IsNil)
   308  
   309  	expectedDbusService := fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "on-failure", "dbus\nBusName=foo.bar.baz", expectedInstallSection)
   310  	c.Assert(string(generatedWrapper), Equals, expectedDbusService)
   311  }
   312  
   313  func (s *servicesWrapperGenSuite) TestGenServiceFileWithBusNameFromSlot(c *C) {
   314  
   315  	yamlText := `
   316  name: snap
   317  version: 1.0
   318  slots:
   319      dbus-slot1:
   320          interface: dbus
   321          bus: system
   322          name: org.example.Foo
   323      dbus-slot2:
   324          interface: dbus
   325          bus: system
   326          name: foo.bar.baz
   327  apps:
   328      app:
   329          command: bin/start
   330          stop-command: bin/stop
   331          reload-command: bin/reload
   332          post-stop-command: bin/stop --post
   333          stop-timeout: 10s
   334          daemon: dbus
   335          activates-on: [dbus-slot1, dbus-slot2]
   336  `
   337  
   338  	info, err := snap.InfoFromSnapYaml([]byte(yamlText))
   339  	c.Assert(err, IsNil)
   340  	info.Revision = snap.R(44)
   341  	app := info.Apps["app"]
   342  
   343  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil)
   344  	c.Assert(err, IsNil)
   345  
   346  	// Bus name defaults to the name from the last slot the daemon
   347  	// activates on.
   348  	c.Assert(string(generatedWrapper), Equals, expectedDbusService)
   349  }
   350  
   351  func (s *servicesWrapperGenSuite) TestGenOneshotServiceFile(c *C) {
   352  
   353  	info := snaptest.MockInfo(c, `
   354  name: snap
   355  version: 1.0
   356  apps:
   357      app:
   358          command: bin/start
   359          stop-command: bin/stop
   360          reload-command: bin/reload
   361          post-stop-command: bin/stop --post
   362          stop-timeout: 10s
   363          daemon: oneshot
   364  `, &snap.SideInfo{Revision: snap.R(44)})
   365  
   366  	app := info.Apps["app"]
   367  
   368  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil)
   369  	c.Assert(err, IsNil)
   370  
   371  	c.Assert(string(generatedWrapper), Equals, expectedOneshotService)
   372  }
   373  
   374  func (s *servicesWrapperGenSuite) TestGenerateSnapUserServiceFile(c *C) {
   375  	yamlText := `
   376  name: snap
   377  version: 1.0
   378  apps:
   379      app:
   380          command: bin/start
   381          stop-command: bin/stop
   382          reload-command: bin/reload
   383          post-stop-command: bin/stop --post
   384          stop-timeout: 10s
   385          daemon: simple
   386          daemon-scope: user
   387  `
   388  	info, err := snap.InfoFromSnapYaml([]byte(yamlText))
   389  	c.Assert(err, IsNil)
   390  	info.Revision = snap.R(44)
   391  	app := info.Apps["app"]
   392  
   393  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil)
   394  	c.Assert(err, IsNil)
   395  	c.Check(string(generatedWrapper), Equals, expectedUserAppService)
   396  }
   397  
   398  func (s *servicesWrapperGenSuite) TestGenerateSnapServiceWithSockets(c *C) {
   399  	const sock1ExpectedFmt = `[Unit]
   400  # Auto-generated, DO NOT EDIT
   401  Description=Socket sock1 for snap application some-snap.app
   402  Requires=%s-some\x2dsnap-44.mount
   403  After=%s-some\x2dsnap-44.mount
   404  X-Snappy=yes
   405  
   406  [Socket]
   407  Service=snap.some-snap.app.service
   408  FileDescriptorName=sock1
   409  ListenStream=%s/sock1.socket
   410  SocketMode=0666
   411  
   412  [Install]
   413  WantedBy=sockets.target
   414  `
   415  	const sock2ExpectedFmt = `[Unit]
   416  # Auto-generated, DO NOT EDIT
   417  Description=Socket sock2 for snap application some-snap.app
   418  Requires=%s-some\x2dsnap-44.mount
   419  After=%s-some\x2dsnap-44.mount
   420  X-Snappy=yes
   421  
   422  [Socket]
   423  Service=snap.some-snap.app.service
   424  FileDescriptorName=sock2
   425  ListenStream=%s/sock2.socket
   426  
   427  [Install]
   428  WantedBy=sockets.target
   429  `
   430  
   431  	si := &snap.Info{
   432  		SuggestedName: "some-snap",
   433  		Version:       "1.0",
   434  		SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   435  	}
   436  	service := &snap.AppInfo{
   437  		Snap:        si,
   438  		Name:        "app",
   439  		Command:     "bin/foo start",
   440  		Daemon:      "simple",
   441  		DaemonScope: snap.SystemDaemon,
   442  		Plugs:       map[string]*snap.PlugInfo{"network-bind": {Interface: "network-bind"}},
   443  		Sockets: map[string]*snap.SocketInfo{
   444  			"sock1": {
   445  				Name:         "sock1",
   446  				ListenStream: "$SNAP_DATA/sock1.socket",
   447  				SocketMode:   0666,
   448  			},
   449  			"sock2": {
   450  				Name:         "sock2",
   451  				ListenStream: "$SNAP_DATA/sock2.socket",
   452  			},
   453  		},
   454  	}
   455  	service.Sockets["sock1"].App = service
   456  	service.Sockets["sock2"].App = service
   457  
   458  	sock1Path := filepath.Join(dirs.SnapServicesDir, "snap.some-snap.app.sock1.socket")
   459  	sock2Path := filepath.Join(dirs.SnapServicesDir, "snap.some-snap.app.sock2.socket")
   460  	sock1Expected := fmt.Sprintf(sock1ExpectedFmt, mountUnitPrefix, mountUnitPrefix, si.DataDir())
   461  	sock2Expected := fmt.Sprintf(sock2ExpectedFmt, mountUnitPrefix, mountUnitPrefix, si.DataDir())
   462  
   463  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(service, nil)
   464  	c.Assert(err, IsNil)
   465  	c.Assert(strings.Contains(string(generatedWrapper), "[Install]"), Equals, false)
   466  	c.Assert(strings.Contains(string(generatedWrapper), "WantedBy=multi-user.target"), Equals, false)
   467  
   468  	generatedSockets, err := wrappers.GenerateSnapSocketFiles(service)
   469  	c.Assert(err, IsNil)
   470  	c.Assert(generatedSockets, Not(IsNil))
   471  	c.Assert(*generatedSockets, HasLen, 2)
   472  	c.Assert(*generatedSockets, DeepEquals, map[string][]byte{
   473  		sock1Path: []byte(sock1Expected),
   474  		sock2Path: []byte(sock2Expected),
   475  	})
   476  }
   477  
   478  func (s *servicesWrapperGenSuite) TestServiceAfterBefore(c *C) {
   479  	const expectedServiceFmt = `[Unit]
   480  # Auto-generated, DO NOT EDIT
   481  Description=Service for snap application snap.app
   482  Requires=%s-snap-44.mount
   483  Wants=network.target
   484  After=%s-snap-44.mount network.target %s snapd.apparmor.service
   485  Before=%s
   486  X-Snappy=yes
   487  
   488  [Service]
   489  EnvironmentFile=-/etc/environment
   490  ExecStart=/usr/bin/snap run snap.app
   491  SyslogIdentifier=snap.app
   492  Restart=%s
   493  WorkingDirectory=/var/snap/snap/44
   494  TimeoutStopSec=30
   495  Type=%s
   496  
   497  [Install]
   498  WantedBy=multi-user.target
   499  `
   500  
   501  	service := &snap.AppInfo{
   502  		Snap: &snap.Info{
   503  			SuggestedName: "snap",
   504  			Version:       "0.3.4",
   505  			SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   506  			Apps: map[string]*snap.AppInfo{
   507  				"foo": {
   508  					Name:        "foo",
   509  					Snap:        &snap.Info{SuggestedName: "snap"},
   510  					Daemon:      "forking",
   511  					DaemonScope: snap.SystemDaemon,
   512  				},
   513  				"bar": {
   514  					Name:        "bar",
   515  					Snap:        &snap.Info{SuggestedName: "snap"},
   516  					Daemon:      "forking",
   517  					DaemonScope: snap.SystemDaemon,
   518  				},
   519  				"zed": {
   520  					Name:        "zed",
   521  					Snap:        &snap.Info{SuggestedName: "snap"},
   522  					Daemon:      "forking",
   523  					DaemonScope: snap.SystemDaemon,
   524  				},
   525  				"baz": {
   526  					Name:        "baz",
   527  					Snap:        &snap.Info{SuggestedName: "snap"},
   528  					Daemon:      "forking",
   529  					DaemonScope: snap.SystemDaemon,
   530  				},
   531  			},
   532  		},
   533  		Name:        "app",
   534  		Command:     "bin/foo start",
   535  		Daemon:      "simple",
   536  		DaemonScope: snap.SystemDaemon,
   537  		StopTimeout: timeout.DefaultTimeout,
   538  	}
   539  
   540  	for _, tc := range []struct {
   541  		after           []string
   542  		before          []string
   543  		generatedAfter  string
   544  		generatedBefore string
   545  	}{{
   546  		after:           []string{"bar", "zed"},
   547  		generatedAfter:  "snap.snap.bar.service snap.snap.zed.service",
   548  		before:          []string{"foo", "baz"},
   549  		generatedBefore: "snap.snap.foo.service snap.snap.baz.service",
   550  	}, {
   551  		after:           []string{"bar"},
   552  		generatedAfter:  "snap.snap.bar.service",
   553  		before:          []string{"foo"},
   554  		generatedBefore: "snap.snap.foo.service",
   555  	},
   556  	} {
   557  		c.Logf("tc: %v", tc)
   558  		service.After = tc.after
   559  		service.Before = tc.before
   560  		generatedWrapper, err := wrappers.GenerateSnapServiceFile(service, nil)
   561  		c.Assert(err, IsNil)
   562  
   563  		expectedService := fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix,
   564  			tc.generatedAfter, tc.generatedBefore, "on-failure", "simple")
   565  		c.Assert(string(generatedWrapper), Equals, expectedService)
   566  	}
   567  }
   568  
   569  func (s *servicesWrapperGenSuite) TestServiceTimerUnit(c *C) {
   570  	const expectedServiceFmt = `[Unit]
   571  # Auto-generated, DO NOT EDIT
   572  Description=Timer app for snap application snap.app
   573  Requires=%s-snap-44.mount
   574  After=%s-snap-44.mount
   575  X-Snappy=yes
   576  
   577  [Timer]
   578  Unit=snap.snap.app.service
   579  OnCalendar=*-*-* 10:00
   580  OnCalendar=*-*-* 11:00
   581  
   582  [Install]
   583  WantedBy=timers.target
   584  `
   585  
   586  	expectedService := fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix)
   587  	service := &snap.AppInfo{
   588  		Snap: &snap.Info{
   589  			SuggestedName: "snap",
   590  			Version:       "0.3.4",
   591  			SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   592  		},
   593  		Name:        "app",
   594  		Command:     "bin/foo start",
   595  		Daemon:      "simple",
   596  		DaemonScope: snap.SystemDaemon,
   597  		StopTimeout: timeout.DefaultTimeout,
   598  		Timer: &snap.TimerInfo{
   599  			Timer: "10:00-12:00/2",
   600  		},
   601  	}
   602  	service.Timer.App = service
   603  
   604  	generatedWrapper, err := wrappers.GenerateSnapTimerFile(service)
   605  	c.Assert(err, IsNil)
   606  
   607  	c.Logf("timer: \n%v\n", string(generatedWrapper))
   608  	c.Assert(string(generatedWrapper), Equals, expectedService)
   609  }
   610  
   611  func (s *servicesWrapperGenSuite) TestServiceTimerUnitBadTimer(c *C) {
   612  	service := &snap.AppInfo{
   613  		Snap: &snap.Info{
   614  			SuggestedName: "snap",
   615  			Version:       "0.3.4",
   616  			SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   617  		},
   618  		Name:        "app",
   619  		Command:     "bin/foo start",
   620  		Daemon:      "simple",
   621  		DaemonScope: snap.SystemDaemon,
   622  		StopTimeout: timeout.DefaultTimeout,
   623  		Timer: &snap.TimerInfo{
   624  			Timer: "bad-timer",
   625  		},
   626  	}
   627  	service.Timer.App = service
   628  
   629  	generatedWrapper, err := wrappers.GenerateSnapTimerFile(service)
   630  	c.Assert(err, ErrorMatches, `cannot parse "bad-timer": "bad" is not a valid weekday`)
   631  	c.Assert(generatedWrapper, IsNil)
   632  }
   633  
   634  func (s *servicesWrapperGenSuite) TestServiceTimerServiceUnit(c *C) {
   635  	const expectedServiceFmt = `[Unit]
   636  # Auto-generated, DO NOT EDIT
   637  Description=Service for snap application snap.app
   638  Requires=%s-snap-44.mount
   639  Wants=network.target
   640  After=%s-snap-44.mount network.target snapd.apparmor.service
   641  X-Snappy=yes
   642  
   643  [Service]
   644  EnvironmentFile=-/etc/environment
   645  ExecStart=/usr/bin/snap run --timer="10:00-12:00,,mon,23:00~01:00/2" snap.app
   646  SyslogIdentifier=snap.app
   647  Restart=%s
   648  WorkingDirectory=/var/snap/snap/44
   649  TimeoutStopSec=30
   650  Type=%s
   651  `
   652  
   653  	expectedService := fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "on-failure", "simple")
   654  	service := &snap.AppInfo{
   655  		Snap: &snap.Info{
   656  			SuggestedName: "snap",
   657  			Version:       "0.3.4",
   658  			SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   659  		},
   660  		Name:        "app",
   661  		Command:     "bin/foo start",
   662  		Daemon:      "simple",
   663  		DaemonScope: snap.SystemDaemon,
   664  		StopTimeout: timeout.DefaultTimeout,
   665  		Timer: &snap.TimerInfo{
   666  			Timer: "10:00-12:00,,mon,23:00~01:00/2",
   667  		},
   668  	}
   669  
   670  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(service, nil)
   671  	c.Assert(err, IsNil)
   672  
   673  	c.Logf("service: \n%v\n", string(generatedWrapper))
   674  	c.Assert(string(generatedWrapper), Equals, expectedService)
   675  }
   676  
   677  func (s *servicesWrapperGenSuite) TestTimerGenerateSchedules(c *C) {
   678  	systemdAnalyzePath, _ := exec.LookPath("systemd-analyze")
   679  	if systemdAnalyzePath != "" {
   680  		// systemd-analyze is in the path, but it will fail if the
   681  		// daemon is not running (as it happens in LP builds) and writes
   682  		// the following to stderr:
   683  		//   Failed to create bus connection: No such file or directory
   684  		cmd := exec.Command(systemdAnalyzePath, "calendar", "12:00")
   685  		err := cmd.Run()
   686  		if err != nil {
   687  			// turns out it's not usable, disable extra verification
   688  			fmt.Fprintln(os.Stderr, `WARNING: systemd-analyze not usable, cannot validate a known schedule "12:00"`)
   689  			systemdAnalyzePath = ""
   690  		}
   691  	}
   692  
   693  	if systemdAnalyzePath == "" {
   694  		fmt.Fprintln(os.Stderr, "WARNING: generated schedules will not be validated by systemd-analyze")
   695  	}
   696  
   697  	for _, t := range []struct {
   698  		in         string
   699  		expected   []string
   700  		randomized bool
   701  	}{{
   702  		in:       "9:00-11:00,,20:00-22:00",
   703  		expected: []string{"*-*-* 09:00", "*-*-* 20:00"},
   704  	}, {
   705  		in:       "9:00-11:00/2,,20:00",
   706  		expected: []string{"*-*-* 09:00", "*-*-* 10:00", "*-*-* 20:00"},
   707  	}, {
   708  		in:         "9:00~11:00/2,,20:00",
   709  		expected:   []string{`\*-\*-\* 09:[0-5][0-9]`, `\*-\*-\* 10:[0-5][0-9]`, `\*-\*-\* 20:00`},
   710  		randomized: true,
   711  	}, {
   712  		in:       "mon,10:00,,fri,15:00",
   713  		expected: []string{"Mon *-*-* 10:00", "Fri *-*-* 15:00"},
   714  	}, {
   715  		in:       "mon-fri,10:00-11:00",
   716  		expected: []string{"Mon,Tue,Wed,Thu,Fri *-*-* 10:00"},
   717  	}, {
   718  		in:       "fri-mon,10:00-11:00",
   719  		expected: []string{"Fri,Sat,Sun,Mon *-*-* 10:00"},
   720  	}, {
   721  		in:       "mon5,10:00",
   722  		expected: []string{"Mon *-*-22,23,24,25,26,27,28,29,30,31 10:00"},
   723  	}, {
   724  		in:       "mon2,10:00",
   725  		expected: []string{"Mon *-*-8,9,10,11,12,13,14 10:00"},
   726  	}, {
   727  		in:       "mon2,mon1,10:00",
   728  		expected: []string{"Mon *-*-8,9,10,11,12,13,14 10:00", "Mon *-*-1,2,3,4,5,6,7 10:00"},
   729  	}, {
   730  		// (deprecated syntax, reduced to mon1-mon)
   731  		// NOTE: non-representable, assumes that service runner does the
   732  		// filtering of when to run the timer
   733  		in:       "mon1-mon3,10:00",
   734  		expected: []string{"*-*-8,9,10,11,12,13,14 10:00", "*-*-1,2,3,4,5,6,7 10:00"},
   735  	}, {
   736  		in:         "mon,10:00~12:00,,fri,15:00",
   737  		expected:   []string{`Mon \*-\*-\* 1[01]:[0-5][0-9]`, `Fri \*-\*-\* 15:00`},
   738  		randomized: true,
   739  	}, {
   740  		in:         "23:00~24:00/4",
   741  		expected:   []string{`\*-\*-\* 23:[01][0-9]`, `\*-\*-\* 23:[12][0-9]`, `\*-\*-\* 23:[34][0-9]`, `*-*-* 23:[45][0-9]`},
   742  		randomized: true,
   743  	}, {
   744  		in:         "23:00~01:00/4",
   745  		expected:   []string{`\*-\*-\* 23:[0-2][0-9]`, `\*-\*-\* 23:[3-5][0-9]`, `\*-\*-\* 00:[0-2][0-9]`, `\*-\*-\* 00:[3-5][0-9]`},
   746  		randomized: true,
   747  	}, {
   748  		in:       "23:00-01:00/4",
   749  		expected: []string{`*-*-* 23:00`, `*-*-* 23:30`, `*-*-* 00:00`, `*-*-* 00:30`},
   750  	}, {
   751  		in:       "24:00",
   752  		expected: []string{`*-*-* 00:00`},
   753  	}, {
   754  		// NOTE: non-representable, assumes that service runner does the
   755  		// filtering of when to run the timer
   756  		in:       "fri-mon1,10:00",
   757  		expected: []string{"*-*-22,23,24,25,26,27,28,29,30,31 10:00", "*-*-1,2,3,4,5,6,7 10:00"},
   758  	}, {
   759  		// NOTE: non-representable, assumes that service runner does the
   760  		// filtering of when to run the timer
   761  		in:       "mon5-fri,10:00",
   762  		expected: []string{"*-*-29,30,31 10:00", "*-*-1,2,3,4,5,6,7 10:00", "*-*-22,23,24,25,26,27,28 10:00"},
   763  	}, {
   764  		// NOTE: non-representable, assumes that service runner does the
   765  		// filtering of when to run the timer
   766  		in:       "mon4-fri,10:00",
   767  		expected: []string{"*-*-29,30,31 10:00", "*-*-1,2,3,4,5,6,7 10:00", "*-*-22,23,24,25,26,27,28 10:00"},
   768  	}, {
   769  		// NOTE: non-representable, assumes that service runner does the
   770  		// filtering of when to run the timer
   771  		in:       "mon-fri2,10:00",
   772  		expected: []string{"*-*-1,2,3,4,5,6,7 10:00", "*-*-8,9,10,11,12,13,14 10:00"},
   773  	}, {
   774  		// NOTE: non-representable, assumes that service runner does the
   775  		// filtering of when to run the timer
   776  		in:       "mon-fri5,10:00",
   777  		expected: []string{"*-*-29,30,31 10:00", "*-*-22,23,24,25,26,27,28 10:00"},
   778  	}, {
   779  		// NOTE: non-representable, assumes that service runner does the
   780  		// filtering of when to run the timer
   781  		in:       "mon1-mon,10:00",
   782  		expected: []string{"*-*-8,9,10,11,12,13,14 10:00", "*-*-1,2,3,4,5,6,7 10:00"},
   783  	}, {
   784  		in:       "mon",
   785  		expected: []string{"Mon *-*-*"},
   786  	}, {
   787  		in:       "mon,fri",
   788  		expected: []string{"Mon *-*-*", "Fri *-*-*"},
   789  	}, {
   790  		in:       "mon2,mon1",
   791  		expected: []string{"Mon *-*-8,9,10,11,12,13,14", "Mon *-*-1,2,3,4,5,6,7"},
   792  	}} {
   793  		c.Logf("trying %+v", t)
   794  
   795  		schedule, err := timeutil.ParseSchedule(t.in)
   796  		c.Check(err, IsNil)
   797  
   798  		timer := wrappers.GenerateOnCalendarSchedules(schedule)
   799  		c.Check(timer, Not(IsNil))
   800  		if !t.randomized {
   801  			c.Check(timer, DeepEquals, t.expected)
   802  		} else {
   803  			c.Assert(timer, HasLen, len(t.expected))
   804  			for i := range timer {
   805  				c.Check(timer[i], Matches, t.expected[i])
   806  			}
   807  		}
   808  
   809  		if systemdAnalyzePath != "" {
   810  			cmd := exec.Command(systemdAnalyzePath, append([]string{"calendar"}, timer...)...)
   811  			out, err := cmd.CombinedOutput()
   812  			c.Check(err, IsNil, Commentf("systemd-analyze failed with output:\n%s", string(out)))
   813  		}
   814  	}
   815  }
   816  
   817  func (s *servicesWrapperGenSuite) TestKillModeSig(c *C) {
   818  	for _, rm := range []string{"sigterm", "sighup", "sigusr1", "sigusr2"} {
   819  		service := &snap.AppInfo{
   820  			Snap: &snap.Info{
   821  				SuggestedName: "snap",
   822  				Version:       "0.3.4",
   823  				SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   824  			},
   825  			Name:        "app",
   826  			Command:     "bin/foo start",
   827  			Daemon:      "simple",
   828  			DaemonScope: snap.SystemDaemon,
   829  			StopMode:    snap.StopModeType(rm),
   830  		}
   831  
   832  		generatedWrapper, err := wrappers.GenerateSnapServiceFile(service, nil)
   833  		c.Assert(err, IsNil)
   834  
   835  		c.Check(string(generatedWrapper), Equals, fmt.Sprintf(`[Unit]
   836  # Auto-generated, DO NOT EDIT
   837  Description=Service for snap application snap.app
   838  Requires=%s-snap-44.mount
   839  Wants=network.target
   840  After=%s-snap-44.mount network.target snapd.apparmor.service
   841  X-Snappy=yes
   842  
   843  [Service]
   844  EnvironmentFile=-/etc/environment
   845  ExecStart=/usr/bin/snap run snap.app
   846  SyslogIdentifier=snap.app
   847  Restart=on-failure
   848  WorkingDirectory=/var/snap/snap/44
   849  TimeoutStopSec=30
   850  Type=simple
   851  KillMode=process
   852  KillSignal=%s
   853  
   854  [Install]
   855  WantedBy=multi-user.target
   856  `, mountUnitPrefix, mountUnitPrefix, strings.ToUpper(rm)))
   857  	}
   858  }
   859  
   860  func (s *servicesWrapperGenSuite) TestRestartDelay(c *C) {
   861  	service := &snap.AppInfo{
   862  		Snap: &snap.Info{
   863  			SuggestedName: "snap",
   864  			Version:       "0.3.4",
   865  			SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   866  		},
   867  		Name:         "app",
   868  		Command:      "bin/foo start",
   869  		Daemon:       "simple",
   870  		DaemonScope:  snap.SystemDaemon,
   871  		RestartDelay: timeout.Timeout(20 * time.Second),
   872  	}
   873  
   874  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(service, nil)
   875  	c.Assert(err, IsNil)
   876  
   877  	c.Check(string(generatedWrapper), Equals, fmt.Sprintf(`[Unit]
   878  # Auto-generated, DO NOT EDIT
   879  Description=Service for snap application snap.app
   880  Requires=%s-snap-44.mount
   881  Wants=network.target
   882  After=%s-snap-44.mount network.target snapd.apparmor.service
   883  X-Snappy=yes
   884  
   885  [Service]
   886  EnvironmentFile=-/etc/environment
   887  ExecStart=/usr/bin/snap run snap.app
   888  SyslogIdentifier=snap.app
   889  Restart=on-failure
   890  RestartSec=20
   891  WorkingDirectory=/var/snap/snap/44
   892  TimeoutStopSec=30
   893  Type=simple
   894  
   895  [Install]
   896  WantedBy=multi-user.target
   897  `, mountUnitPrefix, mountUnitPrefix))
   898  }
   899  
   900  func (s *servicesWrapperGenSuite) TestVitalityScore(c *C) {
   901  	service := &snap.AppInfo{
   902  		Snap: &snap.Info{
   903  			SuggestedName: "snap",
   904  			Version:       "0.3.4",
   905  			SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   906  		},
   907  		Name:         "app",
   908  		Command:      "bin/foo start",
   909  		Daemon:       "simple",
   910  		DaemonScope:  snap.SystemDaemon,
   911  		RestartDelay: timeout.Timeout(20 * time.Second),
   912  	}
   913  
   914  	opts := &wrappers.AddSnapServicesOptions{VitalityRank: 1}
   915  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(service, opts)
   916  	c.Assert(err, IsNil)
   917  
   918  	c.Check(string(generatedWrapper), Equals, fmt.Sprintf(`[Unit]
   919  # Auto-generated, DO NOT EDIT
   920  Description=Service for snap application snap.app
   921  Requires=%s-snap-44.mount
   922  Wants=network.target
   923  After=%s-snap-44.mount network.target snapd.apparmor.service
   924  X-Snappy=yes
   925  
   926  [Service]
   927  EnvironmentFile=-/etc/environment
   928  ExecStart=/usr/bin/snap run snap.app
   929  SyslogIdentifier=snap.app
   930  Restart=on-failure
   931  RestartSec=20
   932  WorkingDirectory=/var/snap/snap/44
   933  TimeoutStopSec=30
   934  Type=simple
   935  OOMScoreAdjust=-899
   936  
   937  [Install]
   938  WantedBy=multi-user.target
   939  `, mountUnitPrefix, mountUnitPrefix))
   940  }