github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/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 }