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