github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/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 InstallMode string 869 870 // TODO: this should go away once we have more plumbing and can change 871 // things vs refactor 872 // https://github.com/snapcore/snapd/pull/794#discussion_r58688496 873 BusName string 874 ActivatesOn []*SlotInfo 875 876 Plugs map[string]*PlugInfo 877 Slots map[string]*SlotInfo 878 Sockets map[string]*SocketInfo 879 880 Environment strutil.OrderedMap 881 882 // list of other service names that this service will start after or 883 // before 884 After []string 885 Before []string 886 887 Timer *TimerInfo 888 889 Autostart string 890 } 891 892 // ScreenshotInfo provides information about a screenshot. 893 type ScreenshotInfo struct { 894 URL string `json:"url,omitempty"` 895 Width int64 `json:"width,omitempty"` 896 Height int64 `json:"height,omitempty"` 897 Note string `json:"note,omitempty"` 898 } 899 900 type MediaInfo struct { 901 Type string `json:"type"` 902 URL string `json:"url"` 903 Width int64 `json:"width,omitempty"` 904 Height int64 `json:"height,omitempty"` 905 } 906 907 type MediaInfos []MediaInfo 908 909 func (mis MediaInfos) IconURL() string { 910 for _, mi := range mis { 911 if mi.Type == "icon" { 912 return mi.URL 913 } 914 } 915 return "" 916 } 917 918 // HookInfo provides information about a hook. 919 type HookInfo struct { 920 Snap *Info 921 922 Name string 923 Plugs map[string]*PlugInfo 924 Slots map[string]*SlotInfo 925 926 Environment strutil.OrderedMap 927 CommandChain []string 928 929 Explicit bool 930 } 931 932 // SystemUsernameInfo provides information about a system username (ie, a 933 // UNIX user and group with the same name). The scope defines visibility of the 934 // username wrt the snap and the system. Defined scopes: 935 // - shared static, snapd-managed user/group shared between host and all 936 // snaps 937 // - private static, snapd-managed user/group private to a particular snap 938 // (currently not implemented) 939 // - external dynamic user/group shared between host and all snaps (currently 940 // not implented) 941 type SystemUsernameInfo struct { 942 Name string 943 Scope string 944 Attrs map[string]interface{} 945 } 946 947 // File returns the path to the *.socket file 948 func (socket *SocketInfo) File() string { 949 return filepath.Join(socket.App.serviceDir(), socket.App.SecurityTag()+"."+socket.Name+".socket") 950 } 951 952 // File returns the path to the *.timer file 953 func (timer *TimerInfo) File() string { 954 return filepath.Join(timer.App.serviceDir(), timer.App.SecurityTag()+".timer") 955 } 956 957 func (app *AppInfo) String() string { 958 return JoinSnapApp(app.Snap.InstanceName(), app.Name) 959 } 960 961 // SecurityTag returns application-specific security tag. 962 // 963 // Security tags are used by various security subsystems as "profile names" and 964 // sometimes also as a part of the file name. 965 func (app *AppInfo) SecurityTag() string { 966 return AppSecurityTag(app.Snap.InstanceName(), app.Name) 967 } 968 969 // DesktopFile returns the path to the installed optional desktop file for the 970 // application. 971 func (app *AppInfo) DesktopFile() string { 972 return filepath.Join(dirs.SnapDesktopFilesDir, fmt.Sprintf("%s_%s.desktop", app.Snap.DesktopPrefix(), app.Name)) 973 } 974 975 // WrapperPath returns the path to wrapper invoking the app binary. 976 func (app *AppInfo) WrapperPath() string { 977 return filepath.Join(dirs.SnapBinariesDir, JoinSnapApp(app.Snap.InstanceName(), app.Name)) 978 } 979 980 // CompleterPath returns the path to the completer snippet for the app binary. 981 func (app *AppInfo) CompleterPath() string { 982 return filepath.Join(dirs.CompletersDir, JoinSnapApp(app.Snap.InstanceName(), app.Name)) 983 } 984 985 func (app *AppInfo) launcherCommand(command string) string { 986 if command != "" { 987 command = " " + command 988 } 989 if app.Name == app.Snap.SnapName() { 990 return fmt.Sprintf("/usr/bin/snap run%s %s", command, app.Snap.InstanceName()) 991 } 992 return fmt.Sprintf("/usr/bin/snap run%s %s.%s", command, app.Snap.InstanceName(), app.Name) 993 } 994 995 // LauncherCommand returns the launcher command line to use when invoking the 996 // app binary. 997 func (app *AppInfo) LauncherCommand() string { 998 if app.Timer != nil { 999 return app.launcherCommand(fmt.Sprintf("--timer=%q", app.Timer.Timer)) 1000 } 1001 return app.launcherCommand("") 1002 } 1003 1004 // LauncherStopCommand returns the launcher command line to use when invoking 1005 // the app stop command binary. 1006 func (app *AppInfo) LauncherStopCommand() string { 1007 return app.launcherCommand("--command=stop") 1008 } 1009 1010 // LauncherReloadCommand returns the launcher command line to use when invoking 1011 // the app stop command binary. 1012 func (app *AppInfo) LauncherReloadCommand() string { 1013 return app.launcherCommand("--command=reload") 1014 } 1015 1016 // LauncherPostStopCommand returns the launcher command line to use when 1017 // invoking the app post-stop command binary. 1018 func (app *AppInfo) LauncherPostStopCommand() string { 1019 return app.launcherCommand("--command=post-stop") 1020 } 1021 1022 // ServiceName returns the systemd service name for the daemon app. 1023 func (app *AppInfo) ServiceName() string { 1024 return app.SecurityTag() + ".service" 1025 } 1026 1027 func (app *AppInfo) serviceDir() string { 1028 switch app.DaemonScope { 1029 case SystemDaemon: 1030 return dirs.SnapServicesDir 1031 case UserDaemon: 1032 return dirs.SnapUserServicesDir 1033 default: 1034 panic("unknown daemon scope") 1035 } 1036 } 1037 1038 // ServiceFile returns the systemd service file path for the daemon app. 1039 func (app *AppInfo) ServiceFile() string { 1040 return filepath.Join(app.serviceDir(), app.ServiceName()) 1041 } 1042 1043 // IsService returns whether app represents a daemon/service. 1044 func (app *AppInfo) IsService() bool { 1045 return app.Daemon != "" 1046 } 1047 1048 // EnvChain returns the chain of environment overrides, possibly with 1049 // expandable $ vars, specific for the app. 1050 func (app *AppInfo) EnvChain() []osutil.ExpandableEnv { 1051 return []osutil.ExpandableEnv{ 1052 {OrderedMap: &app.Snap.Environment}, 1053 {OrderedMap: &app.Environment}, 1054 } 1055 } 1056 1057 // SecurityTag returns the hook-specific security tag. 1058 // 1059 // Security tags are used by various security subsystems as "profile names" and 1060 // sometimes also as a part of the file name. 1061 func (hook *HookInfo) SecurityTag() string { 1062 return HookSecurityTag(hook.Snap.InstanceName(), hook.Name) 1063 } 1064 1065 // EnvChain returns the chain of environment overrides, possibly with 1066 // expandable $ vars, specific for the hook. 1067 func (hook *HookInfo) EnvChain() []osutil.ExpandableEnv { 1068 return []osutil.ExpandableEnv{ 1069 {OrderedMap: &hook.Snap.Environment}, 1070 {OrderedMap: &hook.Environment}, 1071 } 1072 } 1073 1074 func infoFromSnapYamlWithSideInfo(meta []byte, si *SideInfo, strk *scopedTracker) (*Info, error) { 1075 info, err := infoFromSnapYaml(meta, strk) 1076 if err != nil { 1077 return nil, err 1078 } 1079 1080 if si != nil { 1081 info.SideInfo = *si 1082 } 1083 1084 return info, nil 1085 } 1086 1087 // BrokenSnapError describes an error that refers to a snap that warrants the 1088 // "broken" note. 1089 type BrokenSnapError interface { 1090 error 1091 Broken() string 1092 } 1093 1094 type NotFoundError struct { 1095 Snap string 1096 Revision Revision 1097 // Path encodes the path that triggered the not-found error. It may refer to 1098 // a file inside the snap or to the snap file itself. 1099 Path string 1100 } 1101 1102 func (e NotFoundError) Error() string { 1103 if e.Path != "" { 1104 return fmt.Sprintf("cannot find installed snap %q at revision %s: missing file %s", e.Snap, e.Revision, e.Path) 1105 } 1106 return fmt.Sprintf("cannot find installed snap %q at revision %s", e.Snap, e.Revision) 1107 } 1108 1109 func (e NotFoundError) Broken() string { 1110 return e.Error() 1111 } 1112 1113 type invalidMetaError struct { 1114 Snap string 1115 Revision Revision 1116 Msg string 1117 } 1118 1119 func (e invalidMetaError) Error() string { 1120 return fmt.Sprintf("cannot use installed snap %q at revision %s: %s", e.Snap, e.Revision, e.Msg) 1121 } 1122 1123 func (e invalidMetaError) Broken() string { 1124 return e.Error() 1125 } 1126 1127 func MockSanitizePlugsSlots(f func(snapInfo *Info)) (restore func()) { 1128 old := SanitizePlugsSlots 1129 SanitizePlugsSlots = f 1130 return func() { SanitizePlugsSlots = old } 1131 } 1132 1133 var SanitizePlugsSlots = func(snapInfo *Info) { 1134 panic("SanitizePlugsSlots function not set") 1135 } 1136 1137 // ReadInfo reads the snap information for the installed snap with the given 1138 // name and given side-info. 1139 func ReadInfo(name string, si *SideInfo) (*Info, error) { 1140 snapYamlFn := filepath.Join(MountDir(name, si.Revision), "meta", "snap.yaml") 1141 meta, err := ioutil.ReadFile(snapYamlFn) 1142 if os.IsNotExist(err) { 1143 return nil, &NotFoundError{Snap: name, Revision: si.Revision, Path: snapYamlFn} 1144 } 1145 if err != nil { 1146 return nil, err 1147 } 1148 1149 strk := new(scopedTracker) 1150 info, err := infoFromSnapYamlWithSideInfo(meta, si, strk) 1151 if err != nil { 1152 return nil, &invalidMetaError{Snap: name, Revision: si.Revision, Msg: err.Error()} 1153 } 1154 1155 _, instanceKey := SplitInstanceName(name) 1156 info.InstanceKey = instanceKey 1157 1158 err = addImplicitHooks(info) 1159 if err != nil { 1160 return nil, &invalidMetaError{Snap: name, Revision: si.Revision, Msg: err.Error()} 1161 } 1162 1163 bindImplicitHooks(info, strk) 1164 1165 mountFile := MountFile(name, si.Revision) 1166 st, err := os.Lstat(mountFile) 1167 if os.IsNotExist(err) { 1168 // This can happen when "snap try" mode snap is moved around. The mount 1169 // is still in place (it's a bind mount, it doesn't care about the 1170 // source moving) but the symlink in /var/lib/snapd/snaps is now 1171 // dangling. 1172 return nil, &NotFoundError{Snap: name, Revision: si.Revision, Path: mountFile} 1173 } 1174 if err != nil { 1175 return nil, err 1176 } 1177 // If the file is a regular file than it must be a squashfs file that is 1178 // used as the backing store for the snap. The size of that file is the 1179 // size of the snap. 1180 if st.Mode().IsRegular() { 1181 info.Size = st.Size() 1182 } 1183 1184 return info, nil 1185 } 1186 1187 // ReadCurrentInfo reads the snap information from the installed snap in 1188 // 'current' revision 1189 func ReadCurrentInfo(snapName string) (*Info, error) { 1190 curFn := filepath.Join(dirs.SnapMountDir, snapName, "current") 1191 realFn, err := os.Readlink(curFn) 1192 if err != nil { 1193 return nil, fmt.Errorf("cannot find current revision for snap %s: %s", snapName, err) 1194 } 1195 rev := filepath.Base(realFn) 1196 revision, err := ParseRevision(rev) 1197 if err != nil { 1198 return nil, fmt.Errorf("cannot read revision %s: %s", rev, err) 1199 } 1200 1201 return ReadInfo(snapName, &SideInfo{Revision: revision}) 1202 } 1203 1204 // ReadInfoFromSnapFile reads the snap information from the given Container and 1205 // completes it with the given side-info if this is not nil. 1206 func ReadInfoFromSnapFile(snapf Container, si *SideInfo) (*Info, error) { 1207 meta, err := snapf.ReadFile("meta/snap.yaml") 1208 if err != nil { 1209 return nil, err 1210 } 1211 1212 strk := new(scopedTracker) 1213 info, err := infoFromSnapYamlWithSideInfo(meta, si, strk) 1214 if err != nil { 1215 return nil, err 1216 } 1217 1218 info.Size, err = snapf.Size() 1219 if err != nil { 1220 return nil, err 1221 } 1222 1223 err = addImplicitHooksFromContainer(info, snapf) 1224 if err != nil { 1225 return nil, err 1226 } 1227 1228 bindImplicitHooks(info, strk) 1229 1230 err = Validate(info) 1231 if err != nil { 1232 return nil, err 1233 } 1234 1235 return info, nil 1236 } 1237 1238 // InstallDate returns the "install date" of the snap. 1239 // 1240 // If the snap is not active, it'll return a zero time; otherwise it'll return 1241 // the modtime of the "current" symlink. 1242 func InstallDate(name string) time.Time { 1243 cur := filepath.Join(dirs.SnapMountDir, name, "current") 1244 if st, err := os.Lstat(cur); err == nil { 1245 return st.ModTime() 1246 } 1247 return time.Time{} 1248 } 1249 1250 // SplitSnapApp will split a string of the form `snap.app` into the `snap` and 1251 // the `app` part. It also deals with the special case of snapName == appName. 1252 func SplitSnapApp(snapApp string) (snap, app string) { 1253 l := strings.SplitN(snapApp, ".", 2) 1254 if len(l) < 2 { 1255 return l[0], InstanceSnap(l[0]) 1256 } 1257 return l[0], l[1] 1258 } 1259 1260 // JoinSnapApp produces a full application wrapper name from the `snap` and the 1261 // `app` part. It also deals with the special case of snapName == appName. 1262 func JoinSnapApp(snap, app string) string { 1263 storeName, instanceKey := SplitInstanceName(snap) 1264 if storeName == app { 1265 return InstanceName(app, instanceKey) 1266 } 1267 return fmt.Sprintf("%s.%s", snap, app) 1268 } 1269 1270 // InstanceSnap splits the instance name and returns the name of the snap. 1271 func InstanceSnap(instanceName string) string { 1272 snapName, _ := SplitInstanceName(instanceName) 1273 return snapName 1274 } 1275 1276 // SplitInstanceName splits the instance name and returns the snap name and the 1277 // instance key. 1278 func SplitInstanceName(instanceName string) (snapName, instanceKey string) { 1279 split := strings.SplitN(instanceName, "_", 2) 1280 snapName = split[0] 1281 if len(split) > 1 { 1282 instanceKey = split[1] 1283 } 1284 return snapName, instanceKey 1285 } 1286 1287 // InstanceName takes the snap name and the instance key and returns an instance 1288 // name of the snap. 1289 func InstanceName(snapName, instanceKey string) string { 1290 if instanceKey != "" { 1291 return fmt.Sprintf("%s_%s", snapName, instanceKey) 1292 } 1293 return snapName 1294 } 1295 1296 // ByType supports sorting the given slice of snap info by types. The most 1297 // important types will come first. 1298 type ByType []*Info 1299 1300 func (r ByType) Len() int { return len(r) } 1301 func (r ByType) Swap(i, j int) { r[i], r[j] = r[j], r[i] } 1302 func (r ByType) Less(i, j int) bool { 1303 return r[i].Type().SortsBefore(r[j].Type()) 1304 } 1305 1306 // SortServices sorts the apps based on their Before and After specs, such that 1307 // starting the services in the returned ordering will satisfy all specs. 1308 func SortServices(apps []*AppInfo) (sorted []*AppInfo, err error) { 1309 nameToApp := make(map[string]*AppInfo, len(apps)) 1310 for _, app := range apps { 1311 nameToApp[app.Name] = app 1312 } 1313 1314 // list of successors of given app 1315 successors := make(map[string][]*AppInfo, len(apps)) 1316 // count of predecessors (i.e. incoming edges) of given app 1317 predecessors := make(map[string]int, len(apps)) 1318 1319 // identify the successors and predecessors of each app, input data set may 1320 // be a subset of all apps in the snap (eg. when restarting only few select 1321 // apps), thus make sure to look only at those after/before apps that are 1322 // listed in the input 1323 for _, app := range apps { 1324 for _, other := range app.After { 1325 if _, ok := nameToApp[other]; ok { 1326 predecessors[app.Name]++ 1327 successors[other] = append(successors[other], app) 1328 } 1329 } 1330 for _, other := range app.Before { 1331 if _, ok := nameToApp[other]; ok { 1332 predecessors[other]++ 1333 successors[app.Name] = append(successors[app.Name], nameToApp[other]) 1334 } 1335 } 1336 } 1337 1338 // list of apps without predecessors (no incoming edges) 1339 queue := make([]*AppInfo, 0, len(apps)) 1340 for _, app := range apps { 1341 if predecessors[app.Name] == 0 { 1342 queue = append(queue, app) 1343 } 1344 } 1345 1346 // Kahn: 1347 // see https://dl.acm.org/citation.cfm?doid=368996.369025 1348 // https://en.wikipedia.org/wiki/Topological_sorting%23Kahn%27s_algorithm 1349 // 1350 // Apps without predecessors are 'top' nodes. On each iteration, take 1351 // the next 'top' node, and decrease the predecessor count of each 1352 // successor app. Once that successor app has no more predecessors, take 1353 // it out of the predecessors set and add it to the queue of 'top' 1354 // nodes. 1355 for len(queue) > 0 { 1356 app := queue[0] 1357 queue = queue[1:] 1358 for _, successor := range successors[app.Name] { 1359 predecessors[successor.Name]-- 1360 if predecessors[successor.Name] == 0 { 1361 delete(predecessors, successor.Name) 1362 queue = append(queue, successor) 1363 } 1364 } 1365 sorted = append(sorted, app) 1366 } 1367 1368 if len(predecessors) != 0 { 1369 // apps with predecessors unaccounted for are a part of 1370 // dependency cycle 1371 unsatisifed := bytes.Buffer{} 1372 for name := range predecessors { 1373 if unsatisifed.Len() > 0 { 1374 unsatisifed.WriteString(", ") 1375 } 1376 unsatisifed.WriteString(name) 1377 } 1378 return nil, fmt.Errorf("applications are part of a before/after cycle: %s", unsatisifed.String()) 1379 } 1380 return sorted, nil 1381 } 1382 1383 // AppInfoBySnapApp supports sorting the given slice of app infos by 1384 // (instance name, app name). 1385 type AppInfoBySnapApp []*AppInfo 1386 1387 func (a AppInfoBySnapApp) Len() int { return len(a) } 1388 func (a AppInfoBySnapApp) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 1389 func (a AppInfoBySnapApp) Less(i, j int) bool { 1390 iName := a[i].Snap.InstanceName() 1391 jName := a[j].Snap.InstanceName() 1392 if iName == jName { 1393 return a[i].Name < a[j].Name 1394 } 1395 return iName < jName 1396 }