github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/snap/info.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2020 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 snap
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"reflect"
    29  	"sort"
    30  	"strings"
    31  	"time"
    32  
    33  	"github.com/snapcore/snapd/dirs"
    34  	"github.com/snapcore/snapd/osutil"
    35  	"github.com/snapcore/snapd/osutil/sys"
    36  	"github.com/snapcore/snapd/snap/naming"
    37  	"github.com/snapcore/snapd/strutil"
    38  	"github.com/snapcore/snapd/timeout"
    39  )
    40  
    41  // PlaceInfo offers all the information about where a snap and its data are
    42  // located and exposed in the filesystem.
    43  type PlaceInfo interface {
    44  	// InstanceName returns the name of the snap decorated with instance
    45  	// key, if any.
    46  	InstanceName() string
    47  
    48  	// SnapName returns the name of the snap.
    49  	SnapName() string
    50  
    51  	// SnapRevision returns the revision of the snap.
    52  	SnapRevision() Revision
    53  
    54  	// Filename returns the name of the snap with the revision
    55  	// number, as used on the filesystem.
    56  	Filename() string
    57  
    58  	// MountDir returns the base directory of the snap.
    59  	MountDir() string
    60  
    61  	// MountFile returns the path where the snap file that is mounted is
    62  	// installed.
    63  	MountFile() string
    64  
    65  	// HooksDir returns the directory containing the snap's hooks.
    66  	HooksDir() string
    67  
    68  	// DataDir returns the data directory of the snap.
    69  	DataDir() string
    70  
    71  	// UserDataDir returns the per user data directory of the snap.
    72  	UserDataDir(home string) string
    73  
    74  	// CommonDataDir returns the data directory common across revisions of the
    75  	// snap.
    76  	CommonDataDir() string
    77  
    78  	// UserCommonDataDir returns the per user data directory common across
    79  	// revisions of the snap.
    80  	UserCommonDataDir(home string) string
    81  
    82  	// UserXdgRuntimeDir returns the per user XDG_RUNTIME_DIR directory
    83  	UserXdgRuntimeDir(userID sys.UserID) string
    84  
    85  	// DataHomeDir returns the a glob that matches all per user data directories
    86  	// of a snap.
    87  	DataHomeDir() string
    88  
    89  	// CommonDataHomeDir returns a glob that matches all per user data
    90  	// directories common across revisions of the snap.
    91  	CommonDataHomeDir() string
    92  
    93  	// XdgRuntimeDirs returns a glob that matches all XDG_RUNTIME_DIR
    94  	// directories for all users of the snap.
    95  	XdgRuntimeDirs() string
    96  }
    97  
    98  // MinimalPlaceInfo returns a PlaceInfo with just the location information for a
    99  // snap of the given name and revision.
   100  func MinimalPlaceInfo(name string, revision Revision) PlaceInfo {
   101  	storeName, instanceKey := SplitInstanceName(name)
   102  	return &Info{SideInfo: SideInfo{RealName: storeName, Revision: revision}, InstanceKey: instanceKey}
   103  }
   104  
   105  // ParsePlaceInfoFromSnapFileName returns a PlaceInfo with just the location
   106  // information for a snap of file name, failing if the snap file name is invalid
   107  // This explicitly does not support filenames with instance names in them
   108  func ParsePlaceInfoFromSnapFileName(sn string) (PlaceInfo, error) {
   109  	if sn == "" {
   110  		return nil, fmt.Errorf("empty snap file name")
   111  	}
   112  	if strings.Count(sn, "_") > 1 {
   113  		// too many "_", probably has an instance key in the filename like in
   114  		// snap-name_key_23.snap
   115  		return nil, fmt.Errorf("too many '_' in snap file name")
   116  	}
   117  	idx := strings.IndexByte(sn, '_')
   118  	switch {
   119  	case idx < 0:
   120  		return nil, fmt.Errorf("snap file name %q has invalid format (missing '_')", sn)
   121  	case idx == 0:
   122  		return nil, fmt.Errorf("snap file name %q has invalid format (no snap name before '_')", sn)
   123  	}
   124  	// ensure that _ is not the last element
   125  	name := sn[:idx]
   126  	revnoNSuffix := sn[idx+1:]
   127  	rev, err := ParseRevision(strings.TrimSuffix(revnoNSuffix, ".snap"))
   128  	if err != nil {
   129  		return nil, fmt.Errorf("cannot parse revision in snap file name %q: %v", sn, err)
   130  	}
   131  	return &Info{SideInfo: SideInfo{RealName: name, Revision: rev}}, nil
   132  }
   133  
   134  // BaseDir returns the system level directory of given snap.
   135  func BaseDir(name string) string {
   136  	return filepath.Join(dirs.SnapMountDir, name)
   137  }
   138  
   139  // MountDir returns the base directory where it gets mounted of the snap with
   140  // the given name and revision.
   141  func MountDir(name string, revision Revision) string {
   142  	return filepath.Join(BaseDir(name), revision.String())
   143  }
   144  
   145  // MountFile returns the path where the snap file that is mounted is installed.
   146  func MountFile(name string, revision Revision) string {
   147  	return filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%s.snap", name, revision))
   148  }
   149  
   150  // ScopedSecurityTag returns the snap-specific, scope specific, security tag.
   151  func ScopedSecurityTag(snapName, scopeName, suffix string) string {
   152  	return fmt.Sprintf("snap.%s.%s.%s", snapName, scopeName, suffix)
   153  }
   154  
   155  // SecurityTag returns the snap-specific security tag.
   156  func SecurityTag(snapName string) string {
   157  	return fmt.Sprintf("snap.%s", snapName)
   158  }
   159  
   160  // AppSecurityTag returns the application-specific security tag.
   161  func AppSecurityTag(snapName, appName string) string {
   162  	return fmt.Sprintf("%s.%s", SecurityTag(snapName), appName)
   163  }
   164  
   165  // HookSecurityTag returns the hook-specific security tag.
   166  func HookSecurityTag(snapName, hookName string) string {
   167  	return ScopedSecurityTag(snapName, "hook", hookName)
   168  }
   169  
   170  // NoneSecurityTag returns the security tag for interfaces that
   171  // are not associated to an app or hook in the snap.
   172  func NoneSecurityTag(snapName, uniqueName string) string {
   173  	return ScopedSecurityTag(snapName, "none", uniqueName)
   174  }
   175  
   176  // BaseDataDir returns the base directory for snap data locations.
   177  func BaseDataDir(name string) string {
   178  	return filepath.Join(dirs.SnapDataDir, name)
   179  }
   180  
   181  // DataDir returns the data directory for given snap name and revision. The name
   182  // can be
   183  // either a snap name or snap instance name.
   184  func DataDir(name string, revision Revision) string {
   185  	return filepath.Join(BaseDataDir(name), revision.String())
   186  }
   187  
   188  // CommonDataDir returns the common data directory for given snap name. The name
   189  // can be either a snap name or snap instance name.
   190  func CommonDataDir(name string) string {
   191  	return filepath.Join(dirs.SnapDataDir, name, "common")
   192  }
   193  
   194  // HooksDir returns the directory containing the snap's hooks for given snap
   195  // name. The name can be either a snap name or snap instance name.
   196  func HooksDir(name string, revision Revision) string {
   197  	return filepath.Join(MountDir(name, revision), "meta", "hooks")
   198  }
   199  
   200  // UserDataDir returns the user-specific data directory for given snap name. The
   201  // name can be either a snap name or snap instance name.
   202  func UserDataDir(home string, name string, revision Revision) string {
   203  	return filepath.Join(home, dirs.UserHomeSnapDir, name, revision.String())
   204  }
   205  
   206  // UserCommonDataDir returns the user-specific common data directory for given
   207  // snap name. The name can be either a snap name or snap instance name.
   208  func UserCommonDataDir(home string, name string) string {
   209  	return filepath.Join(home, dirs.UserHomeSnapDir, name, "common")
   210  }
   211  
   212  // UserSnapDir returns the user-specific directory for given
   213  // snap name. The name can be either a snap name or snap instance name.
   214  func UserSnapDir(home string, name string) string {
   215  	return filepath.Join(home, dirs.UserHomeSnapDir, name)
   216  }
   217  
   218  // UserXdgRuntimeDir returns the user-specific XDG_RUNTIME_DIR directory for
   219  // given snap name. The name can be either a snap name or snap instance name.
   220  func UserXdgRuntimeDir(euid sys.UserID, name string) string {
   221  	return filepath.Join(dirs.XdgRuntimeDirBase, fmt.Sprintf("%d/snap.%s", euid, name))
   222  }
   223  
   224  // SideInfo holds snap metadata that is crucial for the tracking of
   225  // snaps and for the working of the system offline and which is not
   226  // included in snap.yaml or for which the store is the canonical
   227  // source overriding snap.yaml content.
   228  //
   229  // It can be marshalled and will be stored in the system state for
   230  // each currently installed snap revision so it needs to be evolved
   231  // carefully.
   232  //
   233  // Information that can be taken directly from snap.yaml or that comes
   234  // from the store but is not required for working offline should not
   235  // end up in SideInfo.
   236  type SideInfo struct {
   237  	RealName          string   `yaml:"name,omitempty" json:"name,omitempty"`
   238  	SnapID            string   `yaml:"snap-id" json:"snap-id"`
   239  	Revision          Revision `yaml:"revision" json:"revision"`
   240  	Channel           string   `yaml:"channel,omitempty" json:"channel,omitempty"`
   241  	Contact           string   `yaml:"contact,omitempty" json:"contact,omitempty"`
   242  	EditedTitle       string   `yaml:"title,omitempty" json:"title,omitempty"`
   243  	EditedSummary     string   `yaml:"summary,omitempty" json:"summary,omitempty"`
   244  	EditedDescription string   `yaml:"description,omitempty" json:"description,omitempty"`
   245  	Private           bool     `yaml:"private,omitempty" json:"private,omitempty"`
   246  	Paid              bool     `yaml:"paid,omitempty" json:"paid,omitempty"`
   247  }
   248  
   249  // Info provides information about snaps.
   250  type Info struct {
   251  	SuggestedName string
   252  	InstanceKey   string
   253  	Version       string
   254  	SnapType      Type
   255  	Architectures []string
   256  	Assumes       []string
   257  
   258  	OriginalTitle       string
   259  	OriginalSummary     string
   260  	OriginalDescription string
   261  
   262  	Environment strutil.OrderedMap
   263  
   264  	LicenseAgreement string
   265  	LicenseVersion   string
   266  	License          string
   267  	Epoch            Epoch
   268  	Base             string
   269  	Confinement      ConfinementType
   270  	Apps             map[string]*AppInfo
   271  	LegacyAliases    map[string]*AppInfo // FIXME: eventually drop this
   272  	Hooks            map[string]*HookInfo
   273  	Plugs            map[string]*PlugInfo
   274  	Slots            map[string]*SlotInfo
   275  
   276  	// Plugs or slots with issues (they are not included in Plugs or Slots)
   277  	BadInterfaces map[string]string // slot or plug => message
   278  
   279  	// The information in all the remaining fields is not sourced from the snap
   280  	// blob itself.
   281  	SideInfo
   282  
   283  	// Broken marks whether the snap is broken and the reason.
   284  	Broken string
   285  
   286  	// The information in these fields is ephemeral, available only from the
   287  	// store.
   288  	DownloadInfo
   289  
   290  	Prices  map[string]float64
   291  	MustBuy bool
   292  
   293  	Publisher StoreAccount
   294  
   295  	Media   MediaInfos
   296  	Website string
   297  
   298  	StoreURL string
   299  
   300  	// The flattended channel map with $track/$risk
   301  	Channels map[string]*ChannelSnapInfo
   302  
   303  	// The ordered list of tracks that contain channels
   304  	Tracks []string
   305  
   306  	Layout map[string]*Layout
   307  
   308  	// The list of common-ids from all apps of the snap
   309  	CommonIDs []string
   310  
   311  	// List of system users (usernames) this snap may use. The group of the same
   312  	// name must also exist.
   313  	SystemUsernames map[string]*SystemUsernameInfo
   314  }
   315  
   316  // StoreAccount holds information about a store account, for example of snap
   317  // publisher.
   318  type StoreAccount struct {
   319  	ID          string `json:"id"`
   320  	Username    string `json:"username"`
   321  	DisplayName string `json:"display-name"`
   322  	Validation  string `json:"validation,omitempty"`
   323  }
   324  
   325  // Layout describes a single element of the layout section.
   326  type Layout struct {
   327  	Snap *Info
   328  
   329  	Path     string      `json:"path"`
   330  	Bind     string      `json:"bind,omitempty"`
   331  	BindFile string      `json:"bind-file,omitempty"`
   332  	Type     string      `json:"type,omitempty"`
   333  	User     string      `json:"user,omitempty"`
   334  	Group    string      `json:"group,omitempty"`
   335  	Mode     os.FileMode `json:"mode,omitempty"`
   336  	Symlink  string      `json:"symlink,omitempty"`
   337  }
   338  
   339  // String returns a simple textual representation of a layout.
   340  func (l *Layout) String() string {
   341  	var buf bytes.Buffer
   342  	fmt.Fprintf(&buf, "%s: ", l.Path)
   343  	switch {
   344  	case l.Bind != "":
   345  		fmt.Fprintf(&buf, "bind %s", l.Bind)
   346  	case l.BindFile != "":
   347  		fmt.Fprintf(&buf, "bind-file %s", l.BindFile)
   348  	case l.Symlink != "":
   349  		fmt.Fprintf(&buf, "symlink %s", l.Symlink)
   350  	case l.Type != "":
   351  		fmt.Fprintf(&buf, "type %s", l.Type)
   352  	default:
   353  		fmt.Fprintf(&buf, "???")
   354  	}
   355  	if l.User != "root" && l.User != "" {
   356  		fmt.Fprintf(&buf, ", user: %s", l.User)
   357  	}
   358  	if l.Group != "root" && l.Group != "" {
   359  		fmt.Fprintf(&buf, ", group: %s", l.Group)
   360  	}
   361  	if l.Mode != 0755 {
   362  		fmt.Fprintf(&buf, ", mode: %#o", l.Mode)
   363  	}
   364  	return buf.String()
   365  }
   366  
   367  // ChannelSnapInfo is the minimum information that can be used to clearly
   368  // distinguish different revisions of the same snap.
   369  type ChannelSnapInfo struct {
   370  	Revision    Revision        `json:"revision"`
   371  	Confinement ConfinementType `json:"confinement"`
   372  	Version     string          `json:"version"`
   373  	Channel     string          `json:"channel"`
   374  	Epoch       Epoch           `json:"epoch"`
   375  	Size        int64           `json:"size"`
   376  	ReleasedAt  time.Time       `json:"released-at"`
   377  }
   378  
   379  // InstanceName returns the blessed name of the snap decorated with instance
   380  // key, if any.
   381  func (s *Info) InstanceName() string {
   382  	return InstanceName(s.SnapName(), s.InstanceKey)
   383  }
   384  
   385  // SnapName returns the global blessed name of the snap.
   386  func (s *Info) SnapName() string {
   387  	if s.RealName != "" {
   388  		return s.RealName
   389  	}
   390  	return s.SuggestedName
   391  }
   392  
   393  // Filename returns the name of the snap with the revision number,
   394  // as used on the filesystem. This is the equivalent of
   395  // filepath.Base(s.MountFile()).
   396  func (s *Info) Filename() string {
   397  	return filepath.Base(s.MountFile())
   398  }
   399  
   400  // SnapRevision returns the revision of the snap.
   401  func (s *Info) SnapRevision() Revision {
   402  	return s.Revision
   403  }
   404  
   405  // ID implements naming.SnapRef.
   406  func (s *Info) ID() string {
   407  	return s.SnapID
   408  }
   409  
   410  var _ naming.SnapRef = (*Info)(nil)
   411  
   412  // Title returns the blessed title for the snap.
   413  func (s *Info) Title() string {
   414  	if s.EditedTitle != "" {
   415  		return s.EditedTitle
   416  	}
   417  	return s.OriginalTitle
   418  }
   419  
   420  // Summary returns the blessed summary for the snap.
   421  func (s *Info) Summary() string {
   422  	if s.EditedSummary != "" {
   423  		return s.EditedSummary
   424  	}
   425  	return s.OriginalSummary
   426  }
   427  
   428  // Description returns the blessed description for the snap.
   429  func (s *Info) Description() string {
   430  	if s.EditedDescription != "" {
   431  		return s.EditedDescription
   432  	}
   433  	return s.OriginalDescription
   434  }
   435  
   436  // Type returns the type of the snap, including additional snap ID check
   437  // for the legacy snapd snap definitions.
   438  func (s *Info) Type() Type {
   439  	if s.SnapType == TypeApp && IsSnapd(s.SnapID) {
   440  		return TypeSnapd
   441  	}
   442  	return s.SnapType
   443  }
   444  
   445  // MountDir returns the base directory of the snap where it gets mounted.
   446  func (s *Info) MountDir() string {
   447  	return MountDir(s.InstanceName(), s.Revision)
   448  }
   449  
   450  // MountFile returns the path where the snap file that is mounted is installed.
   451  func (s *Info) MountFile() string {
   452  	return MountFile(s.InstanceName(), s.Revision)
   453  }
   454  
   455  // HooksDir returns the directory containing the snap's hooks.
   456  func (s *Info) HooksDir() string {
   457  	return HooksDir(s.InstanceName(), s.Revision)
   458  }
   459  
   460  // DataDir returns the data directory of the snap.
   461  func (s *Info) DataDir() string {
   462  	return DataDir(s.InstanceName(), s.Revision)
   463  }
   464  
   465  // UserDataDir returns the user-specific data directory of the snap.
   466  func (s *Info) UserDataDir(home string) string {
   467  	return UserDataDir(home, s.InstanceName(), s.Revision)
   468  }
   469  
   470  // UserCommonDataDir returns the user-specific data directory common across
   471  // revision of the snap.
   472  func (s *Info) UserCommonDataDir(home string) string {
   473  	return UserCommonDataDir(home, s.InstanceName())
   474  }
   475  
   476  // CommonDataDir returns the data directory common across revisions of the snap.
   477  func (s *Info) CommonDataDir() string {
   478  	return CommonDataDir(s.InstanceName())
   479  }
   480  
   481  // DataHomeDir returns the per user data directory of the snap.
   482  func (s *Info) DataHomeDir() string {
   483  	return filepath.Join(dirs.SnapDataHomeGlob, s.InstanceName(), s.Revision.String())
   484  }
   485  
   486  // CommonDataHomeDir returns the per user data directory common across revisions
   487  // of the snap.
   488  func (s *Info) CommonDataHomeDir() string {
   489  	return filepath.Join(dirs.SnapDataHomeGlob, s.InstanceName(), "common")
   490  }
   491  
   492  // UserXdgRuntimeDir returns the XDG_RUNTIME_DIR directory of the snap for a
   493  // particular user.
   494  func (s *Info) UserXdgRuntimeDir(euid sys.UserID) string {
   495  	return UserXdgRuntimeDir(euid, s.InstanceName())
   496  }
   497  
   498  // XdgRuntimeDirs returns the XDG_RUNTIME_DIR directories for all users of the
   499  // snap.
   500  func (s *Info) XdgRuntimeDirs() string {
   501  	return filepath.Join(dirs.XdgRuntimeDirGlob, fmt.Sprintf("snap.%s", s.InstanceName()))
   502  }
   503  
   504  // NeedsDevMode returns whether the snap needs devmode.
   505  func (s *Info) NeedsDevMode() bool {
   506  	return s.Confinement == DevModeConfinement
   507  }
   508  
   509  // NeedsClassic  returns whether the snap needs classic confinement consent.
   510  func (s *Info) NeedsClassic() bool {
   511  	return s.Confinement == ClassicConfinement
   512  }
   513  
   514  // Services returns a list of the apps that have "daemon" set.
   515  func (s *Info) Services() []*AppInfo {
   516  	svcs := make([]*AppInfo, 0, len(s.Apps))
   517  	for _, app := range s.Apps {
   518  		if !app.IsService() {
   519  			continue
   520  		}
   521  		svcs = append(svcs, app)
   522  	}
   523  
   524  	return svcs
   525  }
   526  
   527  // ExpandSnapVariables resolves $SNAP, $SNAP_DATA and $SNAP_COMMON inside the
   528  // snap's mount namespace.
   529  func (s *Info) ExpandSnapVariables(path string) string {
   530  	return os.Expand(path, func(v string) string {
   531  		switch v {
   532  		case "SNAP":
   533  			// NOTE: We use dirs.CoreSnapMountDir here as the path used will be
   534  			// always inside the mount namespace snap-confine creates and there
   535  			// we will always have a /snap directory available regardless if the
   536  			// system we're running on supports this or not.
   537  			return filepath.Join(dirs.CoreSnapMountDir, s.SnapName(), s.Revision.String())
   538  		case "SNAP_DATA":
   539  			return DataDir(s.SnapName(), s.Revision)
   540  		case "SNAP_COMMON":
   541  			return CommonDataDir(s.SnapName())
   542  		}
   543  		return ""
   544  	})
   545  }
   546  
   547  // InstallDate returns the "install date" of the snap.
   548  //
   549  // If the snap is not active, it'll return a zero time; otherwise
   550  // it'll return the modtime of the "current" symlink. Sneaky.
   551  func (s *Info) InstallDate() time.Time {
   552  	dir, rev := filepath.Split(s.MountDir())
   553  	cur := filepath.Join(dir, "current")
   554  	tag, err := os.Readlink(cur)
   555  	if err == nil && tag == rev {
   556  		if st, err := os.Lstat(cur); err == nil {
   557  			return st.ModTime()
   558  		}
   559  	}
   560  	return time.Time{}
   561  }
   562  
   563  // IsActive returns whether this snap revision is active.
   564  func (s *Info) IsActive() bool {
   565  	dir, rev := filepath.Split(s.MountDir())
   566  	cur := filepath.Join(dir, "current")
   567  	tag, err := os.Readlink(cur)
   568  	return err == nil && tag == rev
   569  }
   570  
   571  // BadInterfacesSummary returns a summary of the problems of bad plugs
   572  // and slots in the snap.
   573  func BadInterfacesSummary(snapInfo *Info) string {
   574  	inverted := make(map[string][]string)
   575  	for name, reason := range snapInfo.BadInterfaces {
   576  		inverted[reason] = append(inverted[reason], name)
   577  	}
   578  	var buf bytes.Buffer
   579  	fmt.Fprintf(&buf, "snap %q has bad plugs or slots: ", snapInfo.InstanceName())
   580  	reasons := make([]string, 0, len(inverted))
   581  	for reason := range inverted {
   582  		reasons = append(reasons, reason)
   583  	}
   584  	sort.Strings(reasons)
   585  	for _, reason := range reasons {
   586  		names := inverted[reason]
   587  		sort.Strings(names)
   588  		for i, name := range names {
   589  			if i > 0 {
   590  				buf.WriteString(", ")
   591  			}
   592  			buf.WriteString(name)
   593  		}
   594  		fmt.Fprintf(&buf, " (%s); ", reason)
   595  	}
   596  	return strings.TrimSuffix(buf.String(), "; ")
   597  }
   598  
   599  // DesktopPrefix returns the prefix string for the desktop files that
   600  // belongs to the given snapInstance. We need to do something custom
   601  // here because a) we need to be compatible with the world before we had
   602  // parallel installs b) we can't just use the usual "_" parallel installs
   603  // separator because that is already used as the separator between snap
   604  // and desktop filename.
   605  func (s *Info) DesktopPrefix() string {
   606  	if s.InstanceKey == "" {
   607  		return s.SnapName()
   608  	}
   609  	// we cannot use the usual "_" separator because that is also used
   610  	// to separate "$snap_$desktopfile"
   611  	return fmt.Sprintf("%s+%s", s.SnapName(), s.InstanceKey)
   612  }
   613  
   614  // DownloadInfo contains the information to download a snap.
   615  // It can be marshalled.
   616  type DownloadInfo struct {
   617  	AnonDownloadURL string `json:"anon-download-url,omitempty"`
   618  	DownloadURL     string `json:"download-url,omitempty"`
   619  
   620  	Size     int64  `json:"size,omitempty"`
   621  	Sha3_384 string `json:"sha3-384,omitempty"`
   622  
   623  	// The server can include information about available deltas for a given
   624  	// snap at a specific revision during refresh. Currently during refresh the
   625  	// server will provide single matching deltas only, from the clients
   626  	// revision to the target revision when available, per requested format.
   627  	Deltas []DeltaInfo `json:"deltas,omitempty"`
   628  }
   629  
   630  // DeltaInfo contains the information to download a delta
   631  // from one revision to another.
   632  type DeltaInfo struct {
   633  	FromRevision    int    `json:"from-revision,omitempty"`
   634  	ToRevision      int    `json:"to-revision,omitempty"`
   635  	Format          string `json:"format,omitempty"`
   636  	AnonDownloadURL string `json:"anon-download-url,omitempty"`
   637  	DownloadURL     string `json:"download-url,omitempty"`
   638  	Size            int64  `json:"size,omitempty"`
   639  	Sha3_384        string `json:"sha3-384,omitempty"`
   640  }
   641  
   642  // sanity check that Info is a PlaceInfo
   643  var _ PlaceInfo = (*Info)(nil)
   644  
   645  // PlugInfo provides information about a plug.
   646  type PlugInfo struct {
   647  	Snap *Info
   648  
   649  	Name      string
   650  	Interface string
   651  	Attrs     map[string]interface{}
   652  	Label     string
   653  	Apps      map[string]*AppInfo
   654  	Hooks     map[string]*HookInfo
   655  }
   656  
   657  func lookupAttr(attrs map[string]interface{}, path string) (interface{}, bool) {
   658  	var v interface{}
   659  	comps := strings.FieldsFunc(path, func(r rune) bool { return r == '.' })
   660  	if len(comps) == 0 {
   661  		return nil, false
   662  	}
   663  	v = attrs
   664  	for _, comp := range comps {
   665  		m, ok := v.(map[string]interface{})
   666  		if !ok {
   667  			return nil, false
   668  		}
   669  		v, ok = m[comp]
   670  		if !ok {
   671  			return nil, false
   672  		}
   673  	}
   674  
   675  	return v, true
   676  }
   677  
   678  func getAttribute(snapName string, ifaceName string, attrs map[string]interface{}, key string, val interface{}) error {
   679  	v, ok := lookupAttr(attrs, key)
   680  	if !ok {
   681  		return fmt.Errorf("snap %q does not have attribute %q for interface %q", snapName, key, ifaceName)
   682  	}
   683  
   684  	rt := reflect.TypeOf(val)
   685  	if rt.Kind() != reflect.Ptr || val == nil {
   686  		return fmt.Errorf("internal error: cannot get %q attribute of interface %q with non-pointer value", key, ifaceName)
   687  	}
   688  
   689  	if reflect.TypeOf(v) != rt.Elem() {
   690  		return fmt.Errorf("snap %q has interface %q with invalid value type for %q attribute", snapName, ifaceName, key)
   691  	}
   692  	rv := reflect.ValueOf(val)
   693  	rv.Elem().Set(reflect.ValueOf(v))
   694  
   695  	return nil
   696  }
   697  
   698  func (plug *PlugInfo) Attr(key string, val interface{}) error {
   699  	return getAttribute(plug.Snap.InstanceName(), plug.Interface, plug.Attrs, key, val)
   700  }
   701  
   702  func (plug *PlugInfo) Lookup(key string) (interface{}, bool) {
   703  	return lookupAttr(plug.Attrs, key)
   704  }
   705  
   706  // SecurityTags returns security tags associated with a given plug.
   707  func (plug *PlugInfo) SecurityTags() []string {
   708  	tags := make([]string, 0, len(plug.Apps)+len(plug.Hooks))
   709  	for _, app := range plug.Apps {
   710  		tags = append(tags, app.SecurityTag())
   711  	}
   712  	for _, hook := range plug.Hooks {
   713  		tags = append(tags, hook.SecurityTag())
   714  	}
   715  	sort.Strings(tags)
   716  	return tags
   717  }
   718  
   719  // String returns the representation of the plug as snap:plug string.
   720  func (plug *PlugInfo) String() string {
   721  	return fmt.Sprintf("%s:%s", plug.Snap.InstanceName(), plug.Name)
   722  }
   723  
   724  func (slot *SlotInfo) Attr(key string, val interface{}) error {
   725  	return getAttribute(slot.Snap.InstanceName(), slot.Interface, slot.Attrs, key, val)
   726  }
   727  
   728  func (slot *SlotInfo) Lookup(key string) (interface{}, bool) {
   729  	return lookupAttr(slot.Attrs, key)
   730  }
   731  
   732  // SecurityTags returns security tags associated with a given slot.
   733  func (slot *SlotInfo) SecurityTags() []string {
   734  	tags := make([]string, 0, len(slot.Apps))
   735  	for _, app := range slot.Apps {
   736  		tags = append(tags, app.SecurityTag())
   737  	}
   738  	for _, hook := range slot.Hooks {
   739  		tags = append(tags, hook.SecurityTag())
   740  	}
   741  	sort.Strings(tags)
   742  	return tags
   743  }
   744  
   745  // String returns the representation of the slot as snap:slot string.
   746  func (slot *SlotInfo) String() string {
   747  	return fmt.Sprintf("%s:%s", slot.Snap.InstanceName(), slot.Name)
   748  }
   749  
   750  func gatherDefaultContentProvider(providerSnapsToContentTag map[string][]string, plug *PlugInfo) {
   751  	if plug.Interface == "content" {
   752  		var dprovider string
   753  		if err := plug.Attr("default-provider", &dprovider); err == nil && dprovider != "" {
   754  			// usage can be "snap:slot" but slot
   755  			// is ignored/unused
   756  			name := strings.Split(dprovider, ":")[0]
   757  			var contentTag string
   758  			plug.Attr("content", &contentTag)
   759  			tags := providerSnapsToContentTag[name]
   760  			if tags == nil {
   761  				tags = []string{contentTag}
   762  			} else {
   763  				if !strutil.SortedListContains(tags, contentTag) {
   764  					tags = append(tags, contentTag)
   765  					sort.Strings(tags)
   766  				}
   767  			}
   768  			providerSnapsToContentTag[name] = tags
   769  		}
   770  	}
   771  }
   772  
   773  // DefaultContentProviders returns the set of default provider snaps
   774  // requested by the given plugs, mapped to their content tags.
   775  func DefaultContentProviders(plugs []*PlugInfo) (providerSnapsToContentTag map[string][]string) {
   776  	providerSnapsToContentTag = make(map[string][]string)
   777  	for _, plug := range plugs {
   778  		gatherDefaultContentProvider(providerSnapsToContentTag, plug)
   779  	}
   780  	return providerSnapsToContentTag
   781  }
   782  
   783  // SlotInfo provides information about a slot.
   784  type SlotInfo struct {
   785  	Snap *Info
   786  
   787  	Name      string
   788  	Interface string
   789  	Attrs     map[string]interface{}
   790  	Label     string
   791  	Apps      map[string]*AppInfo
   792  	Hooks     map[string]*HookInfo
   793  
   794  	// HotplugKey is a unique key built by the slot's interface
   795  	// using properties of a hotplugged device so that the same
   796  	// slot may be made available if the device is reinserted.
   797  	// It's empty for regular slots.
   798  	HotplugKey HotplugKey
   799  }
   800  
   801  // SocketInfo provides information on application sockets.
   802  type SocketInfo struct {
   803  	App *AppInfo
   804  
   805  	Name         string
   806  	ListenStream string
   807  	SocketMode   os.FileMode
   808  }
   809  
   810  // TimerInfo provides information on application timer.
   811  type TimerInfo struct {
   812  	App *AppInfo
   813  
   814  	Timer string
   815  }
   816  
   817  // StopModeType is the type for the "stop-mode:" of a snap app
   818  type StopModeType string
   819  
   820  // KillAll returns if the stop-mode means all processes should be killed
   821  // when the service is stopped or just the main process.
   822  func (st StopModeType) KillAll() bool {
   823  	return string(st) == "" || strings.HasSuffix(string(st), "-all")
   824  }
   825  
   826  // KillSignal returns the signal that should be used to kill the process
   827  // (or an empty string if no signal is needed).
   828  func (st StopModeType) KillSignal() string {
   829  	if st.Validate() != nil || st == "" {
   830  		return ""
   831  	}
   832  	return strings.ToUpper(strings.TrimSuffix(string(st), "-all"))
   833  }
   834  
   835  // Validate ensures that the StopModeType has an valid value.
   836  func (st StopModeType) Validate() error {
   837  	switch st {
   838  	case "", "sigterm", "sigterm-all", "sighup", "sighup-all", "sigusr1", "sigusr1-all", "sigusr2", "sigusr2-all":
   839  		// valid
   840  		return nil
   841  	}
   842  	return fmt.Errorf(`"stop-mode" field contains invalid value %q`, st)
   843  }
   844  
   845  // AppInfo provides information about an app.
   846  type AppInfo struct {
   847  	Snap *Info
   848  
   849  	Name          string
   850  	LegacyAliases []string // FIXME: eventually drop this
   851  	Command       string
   852  	CommandChain  []string
   853  	CommonID      string
   854  
   855  	Daemon          string
   856  	DaemonScope     DaemonScope
   857  	StopTimeout     timeout.Timeout
   858  	StartTimeout    timeout.Timeout
   859  	WatchdogTimeout timeout.Timeout
   860  	StopCommand     string
   861  	ReloadCommand   string
   862  	PostStopCommand string
   863  	RestartCond     RestartCondition
   864  	RestartDelay    timeout.Timeout
   865  	Completer       string
   866  	RefreshMode     string
   867  	StopMode        StopModeType
   868  
   869  	// TODO: this should go away once we have more plumbing and can change
   870  	// things vs refactor
   871  	// https://github.com/snapcore/snapd/pull/794#discussion_r58688496
   872  	BusName     string
   873  	ActivatesOn []*SlotInfo
   874  
   875  	Plugs   map[string]*PlugInfo
   876  	Slots   map[string]*SlotInfo
   877  	Sockets map[string]*SocketInfo
   878  
   879  	Environment strutil.OrderedMap
   880  
   881  	// list of other service names that this service will start after or
   882  	// before
   883  	After  []string
   884  	Before []string
   885  
   886  	Timer *TimerInfo
   887  
   888  	Autostart string
   889  }
   890  
   891  // ScreenshotInfo provides information about a screenshot.
   892  type ScreenshotInfo struct {
   893  	URL    string `json:"url,omitempty"`
   894  	Width  int64  `json:"width,omitempty"`
   895  	Height int64  `json:"height,omitempty"`
   896  	Note   string `json:"note,omitempty"`
   897  }
   898  
   899  type MediaInfo struct {
   900  	Type   string `json:"type"`
   901  	URL    string `json:"url"`
   902  	Width  int64  `json:"width,omitempty"`
   903  	Height int64  `json:"height,omitempty"`
   904  }
   905  
   906  type MediaInfos []MediaInfo
   907  
   908  func (mis MediaInfos) IconURL() string {
   909  	for _, mi := range mis {
   910  		if mi.Type == "icon" {
   911  			return mi.URL
   912  		}
   913  	}
   914  	return ""
   915  }
   916  
   917  // HookInfo provides information about a hook.
   918  type HookInfo struct {
   919  	Snap *Info
   920  
   921  	Name  string
   922  	Plugs map[string]*PlugInfo
   923  	Slots map[string]*SlotInfo
   924  
   925  	Environment  strutil.OrderedMap
   926  	CommandChain []string
   927  
   928  	Explicit bool
   929  }
   930  
   931  // SystemUsernameInfo provides information about a system username (ie, a
   932  // UNIX user and group with the same name). The scope defines visibility of the
   933  // username wrt the snap and the system. Defined scopes:
   934  // - shared    static, snapd-managed user/group shared between host and all
   935  //             snaps
   936  // - private   static, snapd-managed user/group private to a particular snap
   937  //             (currently not implemented)
   938  // - external  dynamic user/group shared between host and all snaps (currently
   939  //             not implented)
   940  type SystemUsernameInfo struct {
   941  	Name  string
   942  	Scope string
   943  	Attrs map[string]interface{}
   944  }
   945  
   946  // File returns the path to the *.socket file
   947  func (socket *SocketInfo) File() string {
   948  	return filepath.Join(socket.App.serviceDir(), socket.App.SecurityTag()+"."+socket.Name+".socket")
   949  }
   950  
   951  // File returns the path to the *.timer file
   952  func (timer *TimerInfo) File() string {
   953  	return filepath.Join(timer.App.serviceDir(), timer.App.SecurityTag()+".timer")
   954  }
   955  
   956  func (app *AppInfo) String() string {
   957  	return JoinSnapApp(app.Snap.InstanceName(), app.Name)
   958  }
   959  
   960  // SecurityTag returns application-specific security tag.
   961  //
   962  // Security tags are used by various security subsystems as "profile names" and
   963  // sometimes also as a part of the file name.
   964  func (app *AppInfo) SecurityTag() string {
   965  	return AppSecurityTag(app.Snap.InstanceName(), app.Name)
   966  }
   967  
   968  // DesktopFile returns the path to the installed optional desktop file for the
   969  // application.
   970  func (app *AppInfo) DesktopFile() string {
   971  	return filepath.Join(dirs.SnapDesktopFilesDir, fmt.Sprintf("%s_%s.desktop", app.Snap.DesktopPrefix(), app.Name))
   972  }
   973  
   974  // WrapperPath returns the path to wrapper invoking the app binary.
   975  func (app *AppInfo) WrapperPath() string {
   976  	return filepath.Join(dirs.SnapBinariesDir, JoinSnapApp(app.Snap.InstanceName(), app.Name))
   977  }
   978  
   979  // CompleterPath returns the path to the completer snippet for the app binary.
   980  func (app *AppInfo) CompleterPath() string {
   981  	return filepath.Join(dirs.CompletersDir, JoinSnapApp(app.Snap.InstanceName(), app.Name))
   982  }
   983  
   984  func (app *AppInfo) launcherCommand(command string) string {
   985  	if command != "" {
   986  		command = " " + command
   987  	}
   988  	if app.Name == app.Snap.SnapName() {
   989  		return fmt.Sprintf("/usr/bin/snap run%s %s", command, app.Snap.InstanceName())
   990  	}
   991  	return fmt.Sprintf("/usr/bin/snap run%s %s.%s", command, app.Snap.InstanceName(), app.Name)
   992  }
   993  
   994  // LauncherCommand returns the launcher command line to use when invoking the
   995  // app binary.
   996  func (app *AppInfo) LauncherCommand() string {
   997  	if app.Timer != nil {
   998  		return app.launcherCommand(fmt.Sprintf("--timer=%q", app.Timer.Timer))
   999  	}
  1000  	return app.launcherCommand("")
  1001  }
  1002  
  1003  // LauncherStopCommand returns the launcher command line to use when invoking
  1004  // the app stop command binary.
  1005  func (app *AppInfo) LauncherStopCommand() string {
  1006  	return app.launcherCommand("--command=stop")
  1007  }
  1008  
  1009  // LauncherReloadCommand returns the launcher command line to use when invoking
  1010  // the app stop command binary.
  1011  func (app *AppInfo) LauncherReloadCommand() string {
  1012  	return app.launcherCommand("--command=reload")
  1013  }
  1014  
  1015  // LauncherPostStopCommand returns the launcher command line to use when
  1016  // invoking the app post-stop command binary.
  1017  func (app *AppInfo) LauncherPostStopCommand() string {
  1018  	return app.launcherCommand("--command=post-stop")
  1019  }
  1020  
  1021  // ServiceName returns the systemd service name for the daemon app.
  1022  func (app *AppInfo) ServiceName() string {
  1023  	return app.SecurityTag() + ".service"
  1024  }
  1025  
  1026  func (app *AppInfo) serviceDir() string {
  1027  	switch app.DaemonScope {
  1028  	case SystemDaemon:
  1029  		return dirs.SnapServicesDir
  1030  	case UserDaemon:
  1031  		return dirs.SnapUserServicesDir
  1032  	default:
  1033  		panic("unknown daemon scope")
  1034  	}
  1035  }
  1036  
  1037  // ServiceFile returns the systemd service file path for the daemon app.
  1038  func (app *AppInfo) ServiceFile() string {
  1039  	return filepath.Join(app.serviceDir(), app.ServiceName())
  1040  }
  1041  
  1042  // IsService returns whether app represents a daemon/service.
  1043  func (app *AppInfo) IsService() bool {
  1044  	return app.Daemon != ""
  1045  }
  1046  
  1047  // EnvChain returns the chain of environment overrides, possibly with
  1048  // expandable $ vars, specific for the app.
  1049  func (app *AppInfo) EnvChain() []osutil.ExpandableEnv {
  1050  	return []osutil.ExpandableEnv{
  1051  		{OrderedMap: &app.Snap.Environment},
  1052  		{OrderedMap: &app.Environment},
  1053  	}
  1054  }
  1055  
  1056  // SecurityTag returns the hook-specific security tag.
  1057  //
  1058  // Security tags are used by various security subsystems as "profile names" and
  1059  // sometimes also as a part of the file name.
  1060  func (hook *HookInfo) SecurityTag() string {
  1061  	return HookSecurityTag(hook.Snap.InstanceName(), hook.Name)
  1062  }
  1063  
  1064  // EnvChain returns the chain of environment overrides, possibly with
  1065  // expandable $ vars, specific for the hook.
  1066  func (hook *HookInfo) EnvChain() []osutil.ExpandableEnv {
  1067  	return []osutil.ExpandableEnv{
  1068  		{OrderedMap: &hook.Snap.Environment},
  1069  		{OrderedMap: &hook.Environment},
  1070  	}
  1071  }
  1072  
  1073  func infoFromSnapYamlWithSideInfo(meta []byte, si *SideInfo, strk *scopedTracker) (*Info, error) {
  1074  	info, err := infoFromSnapYaml(meta, strk)
  1075  	if err != nil {
  1076  		return nil, err
  1077  	}
  1078  
  1079  	if si != nil {
  1080  		info.SideInfo = *si
  1081  	}
  1082  
  1083  	return info, nil
  1084  }
  1085  
  1086  // BrokenSnapError describes an error that refers to a snap that warrants the
  1087  // "broken" note.
  1088  type BrokenSnapError interface {
  1089  	error
  1090  	Broken() string
  1091  }
  1092  
  1093  type NotFoundError struct {
  1094  	Snap     string
  1095  	Revision Revision
  1096  	// Path encodes the path that triggered the not-found error. It may refer to
  1097  	// a file inside the snap or to the snap file itself.
  1098  	Path string
  1099  }
  1100  
  1101  func (e NotFoundError) Error() string {
  1102  	if e.Path != "" {
  1103  		return fmt.Sprintf("cannot find installed snap %q at revision %s: missing file %s", e.Snap, e.Revision, e.Path)
  1104  	}
  1105  	return fmt.Sprintf("cannot find installed snap %q at revision %s", e.Snap, e.Revision)
  1106  }
  1107  
  1108  func (e NotFoundError) Broken() string {
  1109  	return e.Error()
  1110  }
  1111  
  1112  type invalidMetaError struct {
  1113  	Snap     string
  1114  	Revision Revision
  1115  	Msg      string
  1116  }
  1117  
  1118  func (e invalidMetaError) Error() string {
  1119  	return fmt.Sprintf("cannot use installed snap %q at revision %s: %s", e.Snap, e.Revision, e.Msg)
  1120  }
  1121  
  1122  func (e invalidMetaError) Broken() string {
  1123  	return e.Error()
  1124  }
  1125  
  1126  func MockSanitizePlugsSlots(f func(snapInfo *Info)) (restore func()) {
  1127  	old := SanitizePlugsSlots
  1128  	SanitizePlugsSlots = f
  1129  	return func() { SanitizePlugsSlots = old }
  1130  }
  1131  
  1132  var SanitizePlugsSlots = func(snapInfo *Info) {
  1133  	panic("SanitizePlugsSlots function not set")
  1134  }
  1135  
  1136  // ReadInfo reads the snap information for the installed snap with the given
  1137  // name and given side-info.
  1138  func ReadInfo(name string, si *SideInfo) (*Info, error) {
  1139  	snapYamlFn := filepath.Join(MountDir(name, si.Revision), "meta", "snap.yaml")
  1140  	meta, err := ioutil.ReadFile(snapYamlFn)
  1141  	if os.IsNotExist(err) {
  1142  		return nil, &NotFoundError{Snap: name, Revision: si.Revision, Path: snapYamlFn}
  1143  	}
  1144  	if err != nil {
  1145  		return nil, err
  1146  	}
  1147  
  1148  	strk := new(scopedTracker)
  1149  	info, err := infoFromSnapYamlWithSideInfo(meta, si, strk)
  1150  	if err != nil {
  1151  		return nil, &invalidMetaError{Snap: name, Revision: si.Revision, Msg: err.Error()}
  1152  	}
  1153  
  1154  	_, instanceKey := SplitInstanceName(name)
  1155  	info.InstanceKey = instanceKey
  1156  
  1157  	err = addImplicitHooks(info)
  1158  	if err != nil {
  1159  		return nil, &invalidMetaError{Snap: name, Revision: si.Revision, Msg: err.Error()}
  1160  	}
  1161  
  1162  	bindImplicitHooks(info, strk)
  1163  
  1164  	mountFile := MountFile(name, si.Revision)
  1165  	st, err := os.Lstat(mountFile)
  1166  	if os.IsNotExist(err) {
  1167  		// This can happen when "snap try" mode snap is moved around. The mount
  1168  		// is still in place (it's a bind mount, it doesn't care about the
  1169  		// source moving) but the symlink in /var/lib/snapd/snaps is now
  1170  		// dangling.
  1171  		return nil, &NotFoundError{Snap: name, Revision: si.Revision, Path: mountFile}
  1172  	}
  1173  	if err != nil {
  1174  		return nil, err
  1175  	}
  1176  	// If the file is a regular file than it must be a squashfs file that is
  1177  	// used as the backing store for the snap. The size of that file is the
  1178  	// size of the snap.
  1179  	if st.Mode().IsRegular() {
  1180  		info.Size = st.Size()
  1181  	}
  1182  
  1183  	return info, nil
  1184  }
  1185  
  1186  // ReadCurrentInfo reads the snap information from the installed snap in
  1187  // 'current' revision
  1188  func ReadCurrentInfo(snapName string) (*Info, error) {
  1189  	curFn := filepath.Join(dirs.SnapMountDir, snapName, "current")
  1190  	realFn, err := os.Readlink(curFn)
  1191  	if err != nil {
  1192  		return nil, fmt.Errorf("cannot find current revision for snap %s: %s", snapName, err)
  1193  	}
  1194  	rev := filepath.Base(realFn)
  1195  	revision, err := ParseRevision(rev)
  1196  	if err != nil {
  1197  		return nil, fmt.Errorf("cannot read revision %s: %s", rev, err)
  1198  	}
  1199  
  1200  	return ReadInfo(snapName, &SideInfo{Revision: revision})
  1201  }
  1202  
  1203  // ReadInfoFromSnapFile reads the snap information from the given Container and
  1204  // completes it with the given side-info if this is not nil.
  1205  func ReadInfoFromSnapFile(snapf Container, si *SideInfo) (*Info, error) {
  1206  	meta, err := snapf.ReadFile("meta/snap.yaml")
  1207  	if err != nil {
  1208  		return nil, err
  1209  	}
  1210  
  1211  	strk := new(scopedTracker)
  1212  	info, err := infoFromSnapYamlWithSideInfo(meta, si, strk)
  1213  	if err != nil {
  1214  		return nil, err
  1215  	}
  1216  
  1217  	info.Size, err = snapf.Size()
  1218  	if err != nil {
  1219  		return nil, err
  1220  	}
  1221  
  1222  	err = addImplicitHooksFromContainer(info, snapf)
  1223  	if err != nil {
  1224  		return nil, err
  1225  	}
  1226  
  1227  	bindImplicitHooks(info, strk)
  1228  
  1229  	err = Validate(info)
  1230  	if err != nil {
  1231  		return nil, err
  1232  	}
  1233  
  1234  	return info, nil
  1235  }
  1236  
  1237  // InstallDate returns the "install date" of the snap.
  1238  //
  1239  // If the snap is not active, it'll return a zero time; otherwise it'll return
  1240  // the modtime of the "current" symlink.
  1241  func InstallDate(name string) time.Time {
  1242  	cur := filepath.Join(dirs.SnapMountDir, name, "current")
  1243  	if st, err := os.Lstat(cur); err == nil {
  1244  		return st.ModTime()
  1245  	}
  1246  	return time.Time{}
  1247  }
  1248  
  1249  // SplitSnapApp will split a string of the form `snap.app` into the `snap` and
  1250  // the `app` part. It also deals with the special case of snapName == appName.
  1251  func SplitSnapApp(snapApp string) (snap, app string) {
  1252  	l := strings.SplitN(snapApp, ".", 2)
  1253  	if len(l) < 2 {
  1254  		return l[0], InstanceSnap(l[0])
  1255  	}
  1256  	return l[0], l[1]
  1257  }
  1258  
  1259  // JoinSnapApp produces a full application wrapper name from the `snap` and the
  1260  // `app` part. It also deals with the special case of snapName == appName.
  1261  func JoinSnapApp(snap, app string) string {
  1262  	storeName, instanceKey := SplitInstanceName(snap)
  1263  	if storeName == app {
  1264  		return InstanceName(app, instanceKey)
  1265  	}
  1266  	return fmt.Sprintf("%s.%s", snap, app)
  1267  }
  1268  
  1269  // InstanceSnap splits the instance name and returns the name of the snap.
  1270  func InstanceSnap(instanceName string) string {
  1271  	snapName, _ := SplitInstanceName(instanceName)
  1272  	return snapName
  1273  }
  1274  
  1275  // SplitInstanceName splits the instance name and returns the snap name and the
  1276  // instance key.
  1277  func SplitInstanceName(instanceName string) (snapName, instanceKey string) {
  1278  	split := strings.SplitN(instanceName, "_", 2)
  1279  	snapName = split[0]
  1280  	if len(split) > 1 {
  1281  		instanceKey = split[1]
  1282  	}
  1283  	return snapName, instanceKey
  1284  }
  1285  
  1286  // InstanceName takes the snap name and the instance key and returns an instance
  1287  // name of the snap.
  1288  func InstanceName(snapName, instanceKey string) string {
  1289  	if instanceKey != "" {
  1290  		return fmt.Sprintf("%s_%s", snapName, instanceKey)
  1291  	}
  1292  	return snapName
  1293  }
  1294  
  1295  // ByType supports sorting the given slice of snap info by types. The most
  1296  // important types will come first.
  1297  type ByType []*Info
  1298  
  1299  func (r ByType) Len() int      { return len(r) }
  1300  func (r ByType) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
  1301  func (r ByType) Less(i, j int) bool {
  1302  	return r[i].Type().SortsBefore(r[j].Type())
  1303  }
  1304  
  1305  // SortServices sorts the apps based on their Before and After specs, such that
  1306  // starting the services in the returned ordering will satisfy all specs.
  1307  func SortServices(apps []*AppInfo) (sorted []*AppInfo, err error) {
  1308  	nameToApp := make(map[string]*AppInfo, len(apps))
  1309  	for _, app := range apps {
  1310  		nameToApp[app.Name] = app
  1311  	}
  1312  
  1313  	// list of successors of given app
  1314  	successors := make(map[string][]*AppInfo, len(apps))
  1315  	// count of predecessors (i.e. incoming edges) of given app
  1316  	predecessors := make(map[string]int, len(apps))
  1317  
  1318  	for _, app := range apps {
  1319  		for _, other := range app.After {
  1320  			predecessors[app.Name]++
  1321  			successors[other] = append(successors[other], app)
  1322  		}
  1323  		for _, other := range app.Before {
  1324  			predecessors[other]++
  1325  			successors[app.Name] = append(successors[app.Name], nameToApp[other])
  1326  		}
  1327  	}
  1328  
  1329  	// list of apps without predecessors (no incoming edges)
  1330  	queue := make([]*AppInfo, 0, len(apps))
  1331  	for _, app := range apps {
  1332  		if predecessors[app.Name] == 0 {
  1333  			queue = append(queue, app)
  1334  		}
  1335  	}
  1336  
  1337  	// Kahn:
  1338  	// see https://dl.acm.org/citation.cfm?doid=368996.369025
  1339  	//     https://en.wikipedia.org/wiki/Topological_sorting%23Kahn%27s_algorithm
  1340  	//
  1341  	// Apps without predecessors are 'top' nodes. On each iteration, take
  1342  	// the next 'top' node, and decrease the predecessor count of each
  1343  	// successor app. Once that successor app has no more predecessors, take
  1344  	// it out of the predecessors set and add it to the queue of 'top'
  1345  	// nodes.
  1346  	for len(queue) > 0 {
  1347  		app := queue[0]
  1348  		queue = queue[1:]
  1349  		for _, successor := range successors[app.Name] {
  1350  			predecessors[successor.Name]--
  1351  			if predecessors[successor.Name] == 0 {
  1352  				delete(predecessors, successor.Name)
  1353  				queue = append(queue, successor)
  1354  			}
  1355  		}
  1356  		sorted = append(sorted, app)
  1357  	}
  1358  
  1359  	if len(predecessors) != 0 {
  1360  		// apps with predecessors unaccounted for are a part of
  1361  		// dependency cycle
  1362  		unsatisifed := bytes.Buffer{}
  1363  		for name := range predecessors {
  1364  			if unsatisifed.Len() > 0 {
  1365  				unsatisifed.WriteString(", ")
  1366  			}
  1367  			unsatisifed.WriteString(name)
  1368  		}
  1369  		return nil, fmt.Errorf("applications are part of a before/after cycle: %s", unsatisifed.String())
  1370  	}
  1371  	return sorted, nil
  1372  }
  1373  
  1374  // AppInfoBySnapApp supports sorting the given slice of app infos by
  1375  // (instance name, app name).
  1376  type AppInfoBySnapApp []*AppInfo
  1377  
  1378  func (a AppInfoBySnapApp) Len() int      { return len(a) }
  1379  func (a AppInfoBySnapApp) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
  1380  func (a AppInfoBySnapApp) Less(i, j int) bool {
  1381  	iName := a[i].Snap.InstanceName()
  1382  	jName := a[j].Snap.InstanceName()
  1383  	if iName == jName {
  1384  		return a[i].Name < a[j].Name
  1385  	}
  1386  	return iName < jName
  1387  }