github.com/rigado/snapd@v2.42.5-go-mod+incompatible/wrappers/services.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
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"math/rand"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  	"text/template"
    30  	"time"
    31  
    32  	"github.com/snapcore/snapd/dirs"
    33  	"github.com/snapcore/snapd/logger"
    34  	"github.com/snapcore/snapd/osutil"
    35  	"github.com/snapcore/snapd/osutil/sys"
    36  	"github.com/snapcore/snapd/snap"
    37  	"github.com/snapcore/snapd/systemd"
    38  	"github.com/snapcore/snapd/timeout"
    39  	"github.com/snapcore/snapd/timeutil"
    40  	"github.com/snapcore/snapd/timings"
    41  )
    42  
    43  type interacter interface {
    44  	Notify(status string)
    45  }
    46  
    47  // wait this time between TERM and KILL
    48  var killWait = 5 * time.Second
    49  
    50  func serviceStopTimeout(app *snap.AppInfo) time.Duration {
    51  	tout := app.StopTimeout
    52  	if tout == 0 {
    53  		tout = timeout.DefaultTimeout
    54  	}
    55  	return time.Duration(tout)
    56  }
    57  
    58  func generateSnapServiceFile(app *snap.AppInfo) ([]byte, error) {
    59  	if err := snap.ValidateApp(app); err != nil {
    60  		return nil, err
    61  	}
    62  
    63  	return genServiceFile(app), nil
    64  }
    65  
    66  func stopService(sysd systemd.Systemd, app *snap.AppInfo, inter interacter) error {
    67  	serviceName := app.ServiceName()
    68  	tout := serviceStopTimeout(app)
    69  
    70  	stopErrors := []error{}
    71  	for _, socket := range app.Sockets {
    72  		if err := sysd.Stop(filepath.Base(socket.File()), tout); err != nil {
    73  			stopErrors = append(stopErrors, err)
    74  		}
    75  	}
    76  
    77  	if app.Timer != nil {
    78  		if err := sysd.Stop(filepath.Base(app.Timer.File()), tout); err != nil {
    79  			stopErrors = append(stopErrors, err)
    80  		}
    81  	}
    82  
    83  	if err := sysd.Stop(serviceName, tout); err != nil {
    84  		if !systemd.IsTimeout(err) {
    85  			return err
    86  		}
    87  		inter.Notify(fmt.Sprintf("%s refused to stop, killing.", serviceName))
    88  		// ignore errors for kill; nothing we'd do differently at this point
    89  		sysd.Kill(serviceName, "TERM", "")
    90  		time.Sleep(killWait)
    91  		sysd.Kill(serviceName, "KILL", "")
    92  
    93  	}
    94  
    95  	if len(stopErrors) > 0 {
    96  		return stopErrors[0]
    97  	}
    98  
    99  	return nil
   100  }
   101  
   102  // StartServices starts service units for the applications from the snap which
   103  // are services. Service units will be started in the order provided by the
   104  // caller.
   105  func StartServices(apps []*snap.AppInfo, inter interacter, tm timings.Measurer) (err error) {
   106  	sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter)
   107  
   108  	services := make([]string, 0, len(apps))
   109  	for _, app := range apps {
   110  		// they're *supposed* to be all services, but checking doesn't hurt
   111  		if !app.IsService() {
   112  			continue
   113  		}
   114  
   115  		defer func(app *snap.AppInfo) {
   116  			if err == nil {
   117  				return
   118  			}
   119  			if e := stopService(sysd, app, inter); e != nil {
   120  				inter.Notify(fmt.Sprintf("While trying to stop previously started service %q: %v", app.ServiceName(), e))
   121  			}
   122  			for _, socket := range app.Sockets {
   123  				socketService := filepath.Base(socket.File())
   124  				if e := sysd.Disable(socketService); e != nil {
   125  					inter.Notify(fmt.Sprintf("While trying to disable previously enabled socket service %q: %v", socketService, e))
   126  				}
   127  			}
   128  			if app.Timer != nil {
   129  				timerService := filepath.Base(app.Timer.File())
   130  				if e := sysd.Disable(timerService); e != nil {
   131  					inter.Notify(fmt.Sprintf("While trying to disable previously enabled timer service %q: %v", timerService, e))
   132  				}
   133  			}
   134  		}(app)
   135  
   136  		if len(app.Sockets) == 0 && app.Timer == nil {
   137  			// check if the service is disabled, if so don't start it up
   138  			// this could happen for example if the service was disabled in
   139  			// the install hook by snapctl or if the service was disabled in
   140  			// the previous installation
   141  			isEnabled, err := sysd.IsEnabled(app.ServiceName())
   142  			if err != nil {
   143  				return err
   144  			}
   145  
   146  			if isEnabled {
   147  				services = append(services, app.ServiceName())
   148  			}
   149  		}
   150  
   151  		for _, socket := range app.Sockets {
   152  			socketService := filepath.Base(socket.File())
   153  			// enable the socket
   154  			if err := sysd.Enable(socketService); err != nil {
   155  				return err
   156  			}
   157  
   158  			timings.Run(tm, "start-socket-service", fmt.Sprintf("start socket service %q", socketService), func(nested timings.Measurer) {
   159  				err = sysd.Start(socketService)
   160  			})
   161  			if err != nil {
   162  				return err
   163  			}
   164  		}
   165  
   166  		if app.Timer != nil {
   167  			timerService := filepath.Base(app.Timer.File())
   168  			// enable the timer
   169  			if err := sysd.Enable(timerService); err != nil {
   170  				return err
   171  			}
   172  
   173  			timings.Run(tm, "start-timer-service", fmt.Sprintf("start timer service %q", timerService), func(nested timings.Measurer) {
   174  				err = sysd.Start(timerService)
   175  			})
   176  			if err != nil {
   177  				return err
   178  			}
   179  		}
   180  	}
   181  
   182  	for _, srv := range services {
   183  		// starting all services at once does not create a single
   184  		// transaction, but instead spawns multiple jobs, make sure the
   185  		// services started in the original order by bring them up one
   186  		// by one, see:
   187  		// https://github.com/systemd/systemd/issues/8102
   188  		// https://lists.freedesktop.org/archives/systemd-devel/2018-January/040152.html
   189  		timings.Run(tm, "start-service", fmt.Sprintf("start service %q", srv), func(nested timings.Measurer) {
   190  			err = sysd.Start(srv)
   191  		})
   192  		if err != nil {
   193  			// cleanup was set up by iterating over apps
   194  			return err
   195  		}
   196  	}
   197  
   198  	return nil
   199  }
   200  
   201  // AddSnapServices adds service units for the applications from the snap which are services.
   202  func AddSnapServices(s *snap.Info, inter interacter) (err error) {
   203  	if s.GetType() == snap.TypeSnapd {
   204  		return writeSnapdServicesOnCore(s, inter)
   205  	}
   206  
   207  	sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter)
   208  	var written []string
   209  	var enabled []string
   210  	defer func() {
   211  		if err == nil {
   212  			return
   213  		}
   214  		for _, s := range enabled {
   215  			if e := sysd.Disable(s); e != nil {
   216  				inter.Notify(fmt.Sprintf("while trying to disable %s due to previous failure: %v", s, e))
   217  			}
   218  		}
   219  		for _, s := range written {
   220  			if e := os.Remove(s); e != nil {
   221  				inter.Notify(fmt.Sprintf("while trying to remove %s due to previous failure: %v", s, e))
   222  			}
   223  		}
   224  		if len(written) > 0 {
   225  			if e := sysd.DaemonReload(); e != nil {
   226  				inter.Notify(fmt.Sprintf("while trying to perform systemd daemon-reload due to previous failure: %v", e))
   227  			}
   228  		}
   229  	}()
   230  
   231  	for _, app := range s.Apps {
   232  		if !app.IsService() {
   233  			continue
   234  		}
   235  		// Generate service file
   236  		content, err := generateSnapServiceFile(app)
   237  		if err != nil {
   238  			return err
   239  		}
   240  		svcFilePath := app.ServiceFile()
   241  		os.MkdirAll(filepath.Dir(svcFilePath), 0755)
   242  		if err := osutil.AtomicWriteFile(svcFilePath, content, 0644, 0); err != nil {
   243  			return err
   244  		}
   245  		written = append(written, svcFilePath)
   246  
   247  		// Generate systemd .socket files if needed
   248  		socketFiles, err := generateSnapSocketFiles(app)
   249  		if err != nil {
   250  			return err
   251  		}
   252  		for path, content := range *socketFiles {
   253  			os.MkdirAll(filepath.Dir(path), 0755)
   254  			if err := osutil.AtomicWriteFile(path, content, 0644, 0); err != nil {
   255  				return err
   256  			}
   257  			written = append(written, path)
   258  		}
   259  
   260  		if app.Timer != nil {
   261  			content, err := generateSnapTimerFile(app)
   262  			if err != nil {
   263  				return err
   264  			}
   265  			path := app.Timer.File()
   266  			os.MkdirAll(filepath.Dir(path), 0755)
   267  			if err := osutil.AtomicWriteFile(path, content, 0644, 0); err != nil {
   268  				return err
   269  			}
   270  			written = append(written, path)
   271  		}
   272  
   273  		if app.Timer != nil || len(app.Sockets) != 0 {
   274  			// service is socket or timer activated, not during the
   275  			// boot
   276  			continue
   277  		}
   278  
   279  		svcName := app.ServiceName()
   280  		if err := sysd.Enable(svcName); err != nil {
   281  			return err
   282  		}
   283  		enabled = append(enabled, svcName)
   284  	}
   285  
   286  	if len(written) > 0 {
   287  		if err := sysd.DaemonReload(); err != nil {
   288  			return err
   289  		}
   290  	}
   291  
   292  	return nil
   293  }
   294  
   295  // StopServices stops service units for the applications from the snap which are services.
   296  func StopServices(apps []*snap.AppInfo, reason snap.ServiceStopReason, inter interacter, tm timings.Measurer) error {
   297  	sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter)
   298  
   299  	logger.Debugf("StopServices called for %q, reason: %v", apps, reason)
   300  	for _, app := range apps {
   301  		// Handle the case where service file doesn't exist and don't try to stop it as it will fail.
   302  		// This can happen with snap try when snap.yaml is modified on the fly and a daemon line is added.
   303  		if !app.IsService() || !osutil.FileExists(app.ServiceFile()) {
   304  			continue
   305  		}
   306  		// Skip stop on refresh when refresh mode is set to something
   307  		// other than "restart" (or "" which is the same)
   308  		if reason == snap.StopReasonRefresh {
   309  			logger.Debugf(" %s refresh-mode: %v", app.Name, app.StopMode)
   310  			switch app.RefreshMode {
   311  			case "endure":
   312  				// skip this service
   313  				continue
   314  			}
   315  		}
   316  
   317  		var err error
   318  		timings.Run(tm, "stop-service", fmt.Sprintf("stop service %q", app.ServiceName()), func(nested timings.Measurer) {
   319  			err = stopService(sysd, app, inter)
   320  		})
   321  		if err != nil {
   322  			return err
   323  		}
   324  
   325  		// ensure the service is really stopped on remove regardless
   326  		// of stop-mode
   327  		if reason == snap.StopReasonRemove && !app.StopMode.KillAll() {
   328  			// FIXME: make this smarter and avoid the killWait
   329  			//        delay if not needed (i.e. if all processes
   330  			//        have died)
   331  			sysd.Kill(app.ServiceName(), "TERM", "all")
   332  			time.Sleep(killWait)
   333  			sysd.Kill(app.ServiceName(), "KILL", "")
   334  		}
   335  	}
   336  
   337  	return nil
   338  }
   339  
   340  // ServicesEnableState returns a map of service names from the given snap,
   341  // together with their enable/disable status.
   342  func ServicesEnableState(s *snap.Info, inter interacter) (map[string]bool, error) {
   343  	sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter)
   344  
   345  	// loop over all services in the snap, querying systemd for the current
   346  	// systemd state of the snaps
   347  	snapSvcsState := make(map[string]bool, len(s.Apps))
   348  	for name, app := range s.Apps {
   349  		if !app.IsService() {
   350  			continue
   351  		}
   352  		state, err := sysd.IsEnabled(app.ServiceName())
   353  		if err != nil {
   354  			return nil, err
   355  		}
   356  		snapSvcsState[name] = state
   357  	}
   358  	return snapSvcsState, nil
   359  }
   360  
   361  // RemoveSnapServices disables and removes service units for the applications from the snap which are services.
   362  func RemoveSnapServices(s *snap.Info, inter interacter) error {
   363  	sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter)
   364  	nservices := 0
   365  
   366  	for _, app := range s.Apps {
   367  		if !app.IsService() || !osutil.FileExists(app.ServiceFile()) {
   368  			continue
   369  		}
   370  		nservices++
   371  
   372  		serviceName := filepath.Base(app.ServiceFile())
   373  
   374  		for _, socket := range app.Sockets {
   375  			path := socket.File()
   376  			socketServiceName := filepath.Base(path)
   377  			if err := sysd.Disable(socketServiceName); err != nil {
   378  				return err
   379  			}
   380  
   381  			if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
   382  				logger.Noticef("Failed to remove socket file %q for %q: %v", path, serviceName, err)
   383  			}
   384  		}
   385  
   386  		if app.Timer != nil {
   387  			path := app.Timer.File()
   388  
   389  			timerName := filepath.Base(path)
   390  			if err := sysd.Disable(timerName); err != nil {
   391  				return err
   392  			}
   393  
   394  			if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
   395  				logger.Noticef("Failed to remove timer file %q for %q: %v", path, serviceName, err)
   396  			}
   397  		}
   398  
   399  		if err := sysd.Disable(serviceName); err != nil {
   400  			return err
   401  		}
   402  
   403  		if err := os.Remove(app.ServiceFile()); err != nil && !os.IsNotExist(err) {
   404  			logger.Noticef("Failed to remove service file for %q: %v", serviceName, err)
   405  		}
   406  
   407  	}
   408  
   409  	// only reload if we actually had services
   410  	if nservices > 0 {
   411  		if err := sysd.DaemonReload(); err != nil {
   412  			return err
   413  		}
   414  	}
   415  
   416  	return nil
   417  }
   418  
   419  func genServiceNames(snap *snap.Info, appNames []string) []string {
   420  	names := make([]string, 0, len(appNames))
   421  
   422  	for _, name := range appNames {
   423  		if app := snap.Apps[name]; app != nil {
   424  			names = append(names, app.ServiceName())
   425  		}
   426  	}
   427  	return names
   428  }
   429  
   430  func genServiceFile(appInfo *snap.AppInfo) []byte {
   431  	serviceTemplate := `[Unit]
   432  # Auto-generated, DO NOT EDIT
   433  Description=Service for snap application {{.App.Snap.InstanceName}}.{{.App.Name}}
   434  Requires={{.MountUnit}}
   435  Wants={{.PrerequisiteTarget}}
   436  After={{.MountUnit}} {{.PrerequisiteTarget}}{{if .After}} {{ stringsJoin .After " " }}{{end}}
   437  {{- if .Before}}
   438  Before={{ stringsJoin .Before " "}}
   439  {{- end}}
   440  X-Snappy=yes
   441  
   442  [Service]
   443  ExecStart={{.App.LauncherCommand}}
   444  SyslogIdentifier={{.App.Snap.InstanceName}}.{{.App.Name}}
   445  Restart={{.Restart}}
   446  {{- if .App.RestartDelay}}
   447  RestartSec={{.App.RestartDelay.Seconds}}
   448  {{- end}}
   449  WorkingDirectory={{.App.Snap.DataDir}}
   450  {{- if .App.StopCommand}}
   451  ExecStop={{.App.LauncherStopCommand}}
   452  {{- end}}
   453  {{- if .App.ReloadCommand}}
   454  ExecReload={{.App.LauncherReloadCommand}}
   455  {{- end}}
   456  {{- if .App.PostStopCommand}}
   457  ExecStopPost={{.App.LauncherPostStopCommand}}
   458  {{- end}}
   459  {{- if .StopTimeout}}
   460  TimeoutStopSec={{.StopTimeout.Seconds}}
   461  {{- end}}
   462  {{- if .StartTimeout}}
   463  TimeoutStartSec={{.StartTimeout.Seconds}}
   464  {{- end}}
   465  Type={{.App.Daemon}}
   466  {{- if .Remain}}
   467  RemainAfterExit={{.Remain}}
   468  {{- end}}
   469  {{- if .App.BusName}}
   470  BusName={{.App.BusName}}
   471  {{- end}}
   472  {{- if .App.WatchdogTimeout}}
   473  WatchdogSec={{.App.WatchdogTimeout.Seconds}}
   474  {{- end}}
   475  {{- if .KillMode}}
   476  KillMode={{.KillMode}}
   477  {{- end}}
   478  {{- if .KillSignal}}
   479  KillSignal={{.KillSignal}}
   480  {{- end}}
   481  {{- if not .App.Sockets}}
   482  
   483  [Install]
   484  WantedBy={{.ServicesTarget}}
   485  {{- end}}
   486  `
   487  	var templateOut bytes.Buffer
   488  	tmpl := template.New("service-wrapper")
   489  	tmpl.Funcs(template.FuncMap{
   490  		"stringsJoin": strings.Join,
   491  	})
   492  	t := template.Must(tmpl.Parse(serviceTemplate))
   493  
   494  	restartCond := appInfo.RestartCond.String()
   495  	if restartCond == "" {
   496  		restartCond = snap.RestartOnFailure.String()
   497  	}
   498  
   499  	var remain string
   500  	if appInfo.Daemon == "oneshot" {
   501  		// any restart condition other than "no" is invalid for oneshot daemons
   502  		restartCond = "no"
   503  		// If StopExec is present for a oneshot service than we also need
   504  		// RemainAfterExit=yes
   505  		if appInfo.StopCommand != "" {
   506  			remain = "yes"
   507  		}
   508  	}
   509  	var killMode string
   510  	if !appInfo.StopMode.KillAll() {
   511  		killMode = "process"
   512  	}
   513  
   514  	wrapperData := struct {
   515  		App *snap.AppInfo
   516  
   517  		Restart            string
   518  		StopTimeout        time.Duration
   519  		StartTimeout       time.Duration
   520  		ServicesTarget     string
   521  		PrerequisiteTarget string
   522  		MountUnit          string
   523  		Remain             string
   524  		KillMode           string
   525  		KillSignal         string
   526  		Before             []string
   527  		After              []string
   528  
   529  		Home    string
   530  		EnvVars string
   531  	}{
   532  		App: appInfo,
   533  
   534  		Restart:            restartCond,
   535  		StopTimeout:        serviceStopTimeout(appInfo),
   536  		StartTimeout:       time.Duration(appInfo.StartTimeout),
   537  		ServicesTarget:     systemd.ServicesTarget,
   538  		PrerequisiteTarget: systemd.PrerequisiteTarget,
   539  		MountUnit:          filepath.Base(systemd.MountUnitPath(appInfo.Snap.MountDir())),
   540  		Remain:             remain,
   541  		KillMode:           killMode,
   542  		KillSignal:         appInfo.StopMode.KillSignal(),
   543  
   544  		Before: genServiceNames(appInfo.Snap, appInfo.Before),
   545  		After:  genServiceNames(appInfo.Snap, appInfo.After),
   546  
   547  		// systemd runs as PID 1 so %h will not work.
   548  		Home: "/root",
   549  	}
   550  
   551  	if err := t.Execute(&templateOut, wrapperData); err != nil {
   552  		// this can never happen, except we forget a variable
   553  		logger.Panicf("Unable to execute template: %v", err)
   554  	}
   555  
   556  	return templateOut.Bytes()
   557  }
   558  
   559  func genServiceSocketFile(appInfo *snap.AppInfo, socketName string) []byte {
   560  	socketTemplate := `[Unit]
   561  # Auto-generated, DO NOT EDIT
   562  Description=Socket {{.SocketName}} for snap application {{.App.Snap.InstanceName}}.{{.App.Name}}
   563  Requires={{.MountUnit}}
   564  After={{.MountUnit}}
   565  X-Snappy=yes
   566  
   567  [Socket]
   568  Service={{.ServiceFileName}}
   569  FileDescriptorName={{.SocketInfo.Name}}
   570  ListenStream={{.ListenStream}}
   571  {{- if .SocketInfo.SocketMode}}
   572  SocketMode={{.SocketInfo.SocketMode | printf "%04o"}}
   573  {{- end}}
   574  
   575  [Install]
   576  WantedBy={{.SocketsTarget}}
   577  `
   578  	var templateOut bytes.Buffer
   579  	t := template.Must(template.New("socket-wrapper").Parse(socketTemplate))
   580  
   581  	socket := appInfo.Sockets[socketName]
   582  	listenStream := renderListenStream(socket)
   583  	wrapperData := struct {
   584  		App             *snap.AppInfo
   585  		ServiceFileName string
   586  		SocketsTarget   string
   587  		MountUnit       string
   588  		SocketName      string
   589  		SocketInfo      *snap.SocketInfo
   590  		ListenStream    string
   591  	}{
   592  		App:             appInfo,
   593  		ServiceFileName: filepath.Base(appInfo.ServiceFile()),
   594  		SocketsTarget:   systemd.SocketsTarget,
   595  		MountUnit:       filepath.Base(systemd.MountUnitPath(appInfo.Snap.MountDir())),
   596  		SocketName:      socketName,
   597  		SocketInfo:      socket,
   598  		ListenStream:    listenStream,
   599  	}
   600  
   601  	if err := t.Execute(&templateOut, wrapperData); err != nil {
   602  		// this can never happen, except we forget a variable
   603  		logger.Panicf("Unable to execute template: %v", err)
   604  	}
   605  
   606  	return templateOut.Bytes()
   607  }
   608  
   609  func generateSnapSocketFiles(app *snap.AppInfo) (*map[string][]byte, error) {
   610  	if err := snap.ValidateApp(app); err != nil {
   611  		return nil, err
   612  	}
   613  
   614  	socketFiles := make(map[string][]byte)
   615  	for name, socket := range app.Sockets {
   616  		socketFiles[socket.File()] = genServiceSocketFile(app, name)
   617  	}
   618  	return &socketFiles, nil
   619  }
   620  
   621  func renderListenStream(socket *snap.SocketInfo) string {
   622  	snap := socket.App.Snap
   623  	listenStream := strings.Replace(socket.ListenStream, "$SNAP_DATA", snap.DataDir(), -1)
   624  	// TODO: when we support User/Group in the generated systemd unit,
   625  	// adjust this accordingly
   626  	serviceUserUid := sys.UserID(0)
   627  	runtimeDir := snap.UserXdgRuntimeDir(serviceUserUid)
   628  	listenStream = strings.Replace(listenStream, "$XDG_RUNTIME_DIR", runtimeDir, -1)
   629  	return strings.Replace(listenStream, "$SNAP_COMMON", snap.CommonDataDir(), -1)
   630  }
   631  
   632  func generateSnapTimerFile(app *snap.AppInfo) ([]byte, error) {
   633  	timerTemplate := `[Unit]
   634  # Auto-generated, DO NOT EDIT
   635  Description=Timer {{.TimerName}} for snap application {{.App.Snap.InstanceName}}.{{.App.Name}}
   636  Requires={{.MountUnit}}
   637  After={{.MountUnit}}
   638  X-Snappy=yes
   639  
   640  [Timer]
   641  Unit={{.ServiceFileName}}
   642  {{ range .Schedules }}OnCalendar={{ . }}
   643  {{ end }}
   644  [Install]
   645  WantedBy={{.TimersTarget}}
   646  `
   647  	var templateOut bytes.Buffer
   648  	t := template.Must(template.New("timer-wrapper").Parse(timerTemplate))
   649  
   650  	timerSchedule, err := timeutil.ParseSchedule(app.Timer.Timer)
   651  	if err != nil {
   652  		return nil, err
   653  	}
   654  
   655  	schedules := generateOnCalendarSchedules(timerSchedule)
   656  
   657  	wrapperData := struct {
   658  		App             *snap.AppInfo
   659  		ServiceFileName string
   660  		TimersTarget    string
   661  		TimerName       string
   662  		MountUnit       string
   663  		Schedules       []string
   664  	}{
   665  		App:             app,
   666  		ServiceFileName: filepath.Base(app.ServiceFile()),
   667  		TimersTarget:    systemd.TimersTarget,
   668  		TimerName:       app.Name,
   669  		MountUnit:       filepath.Base(systemd.MountUnitPath(app.Snap.MountDir())),
   670  		Schedules:       schedules,
   671  	}
   672  
   673  	if err := t.Execute(&templateOut, wrapperData); err != nil {
   674  		// this can never happen, except we forget a variable
   675  		logger.Panicf("Unable to execute template: %v", err)
   676  	}
   677  
   678  	return templateOut.Bytes(), nil
   679  
   680  }
   681  
   682  func makeAbbrevWeekdays(start time.Weekday, end time.Weekday) []string {
   683  	out := make([]string, 0, 7)
   684  	for w := start; w%7 != (end + 1); w++ {
   685  		out = append(out, time.Weekday(w % 7).String()[0:3])
   686  	}
   687  	return out
   688  }
   689  
   690  // generateOnCalendarSchedules converts a schedule into OnCalendar schedules
   691  // suitable for use in systemd *.timer units using systemd.time(7)
   692  // https://www.freedesktop.org/software/systemd/man/systemd.time.html
   693  func generateOnCalendarSchedules(schedule []*timeutil.Schedule) []string {
   694  	calendarEvents := make([]string, 0, len(schedule))
   695  	for _, sched := range schedule {
   696  		days := make([]string, 0, len(sched.WeekSpans))
   697  		for _, week := range sched.WeekSpans {
   698  			abbrev := strings.Join(makeAbbrevWeekdays(week.Start.Weekday, week.End.Weekday), ",")
   699  			switch week.Start.Pos {
   700  			case timeutil.EveryWeek:
   701  				// eg: mon, mon-fri, fri-mon
   702  				days = append(days, fmt.Sprintf("%s *-*-*", abbrev))
   703  			case timeutil.LastWeek:
   704  				// eg: mon5
   705  				days = append(days, fmt.Sprintf("%s *-*~7/1", abbrev))
   706  			default:
   707  				// eg: mon1, fri1, mon1-tue2
   708  				startDay := (week.Start.Pos-1)*7 + 1
   709  				endDay := week.End.Pos * 7
   710  
   711  				// NOTE: schedule mon1-tue2 (all weekdays
   712  				// between the first Monday of the month, until
   713  				// the second Tuesday of the month) is not
   714  				// translatable to systemd.time(7) format, for
   715  				// this assume all weekdays and allow the runner
   716  				// to do the filtering
   717  				if week.Start != week.End {
   718  					days = append(days,
   719  						fmt.Sprintf("*-*-%d..%d/1", startDay, endDay))
   720  				} else {
   721  					days = append(days,
   722  						fmt.Sprintf("%s *-*-%d..%d/1", abbrev, startDay, endDay))
   723  				}
   724  
   725  			}
   726  		}
   727  
   728  		if len(days) == 0 {
   729  			// no weekday spec, meaning the timer runs every day
   730  			days = []string{"*-*-*"}
   731  		}
   732  
   733  		startTimes := make([]string, 0, len(sched.ClockSpans))
   734  		for _, clocks := range sched.ClockSpans {
   735  			// use expanded clock spans
   736  			for _, span := range clocks.ClockSpans() {
   737  				when := span.Start
   738  				if span.Spread {
   739  					length := span.End.Sub(span.Start)
   740  					if length < 0 {
   741  						// span Start wraps around, so we have '00:00.Sub(23:45)'
   742  						length = -length
   743  					}
   744  					if length > 5*time.Minute {
   745  						// replicate what timeutil.Next() does
   746  						// and cut some time at the end of the
   747  						// window so that events do not happen
   748  						// directly one after another
   749  						length -= 5 * time.Minute
   750  					}
   751  					when = when.Add(time.Duration(rand.Int63n(int64(length))))
   752  				}
   753  				if when.Hour == 24 {
   754  					// 24:00 for us means the other end of
   755  					// the day, for systemd we need to
   756  					// adjust it to the 0-23 hour range
   757  					when.Hour -= 24
   758  				}
   759  
   760  				startTimes = append(startTimes, when.String())
   761  			}
   762  		}
   763  
   764  		for _, day := range days {
   765  			for _, startTime := range startTimes {
   766  				calendarEvents = append(calendarEvents, fmt.Sprintf("%s %s", day, startTime))
   767  			}
   768  		}
   769  	}
   770  	return calendarEvents
   771  }