Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| // -*- Mode: Go; indent-tabs-mode: t -*- | |
| /* | |
| * Copyright (C) 2016-2017 Canonical Ltd | |
| * | |
| * This program is free software: you can redistribute it and/or modify | |
| * it under the terms of the GNU General Public License version 3 as | |
| * published by the Free Software Foundation. | |
| * | |
| * This program is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| * GNU General Public License for more details. | |
| * | |
| * You should have received a copy of the GNU General Public License | |
| * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| * | |
| */ | |
| package snapstate | |
| import ( | |
| "errors" | |
| "fmt" | |
| "os" | |
| "time" | |
| "gopkg.in/tomb.v2" | |
| "github.com/snapcore/snapd/dirs" | |
| "github.com/snapcore/snapd/errtracker" | |
| "github.com/snapcore/snapd/i18n" | |
| "github.com/snapcore/snapd/overlord/snapstate/backend" | |
| "github.com/snapcore/snapd/overlord/state" | |
| "github.com/snapcore/snapd/release" | |
| "github.com/snapcore/snapd/snap" | |
| "github.com/snapcore/snapd/store" | |
| ) | |
| // overridden in the tests | |
| var errtrackerReport = errtracker.Report | |
| // SnapManager is responsible for the installation and removal of snaps. | |
| type SnapManager struct { | |
| state *state.State | |
| backend managerBackend | |
| autoRefresh *autoRefresh | |
| refreshHints *refreshHints | |
| catalogRefresh *catalogRefresh | |
| lastUbuntuCoreTransitionAttempt time.Time | |
| runner *state.TaskRunner | |
| } | |
| // SnapSetup holds the necessary snap details to perform most snap manager tasks. | |
| type SnapSetup struct { | |
| // FIXME: rename to RequestedChannel to convey the meaning better | |
| Channel string `json:"channel,omitempty"` | |
| UserID int `json:"user-id,omitempty"` | |
| Base string `json:"base,omitempty"` | |
| Flags | |
| SnapPath string `json:"snap-path,omitempty"` | |
| DownloadInfo *snap.DownloadInfo `json:"download-info,omitempty"` | |
| SideInfo *snap.SideInfo `json:"side-info,omitempty"` | |
| } | |
| func (snapsup *SnapSetup) Name() string { | |
| if snapsup.SideInfo.RealName == "" { | |
| panic("SnapSetup.SideInfo.RealName not set") | |
| } | |
| return snapsup.SideInfo.RealName | |
| } | |
| func (snapsup *SnapSetup) Revision() snap.Revision { | |
| return snapsup.SideInfo.Revision | |
| } | |
| func (snapsup *SnapSetup) placeInfo() snap.PlaceInfo { | |
| return snap.MinimalPlaceInfo(snapsup.Name(), snapsup.Revision()) | |
| } | |
| func (snapsup *SnapSetup) MountDir() string { | |
| return snap.MountDir(snapsup.Name(), snapsup.Revision()) | |
| } | |
| func (snapsup *SnapSetup) MountFile() string { | |
| return snap.MountFile(snapsup.Name(), snapsup.Revision()) | |
| } | |
| // SnapState holds the state for a snap installed in the system. | |
| type SnapState struct { | |
| SnapType string `json:"type"` // Use Type and SetType | |
| Sequence []*snap.SideInfo `json:"sequence"` | |
| Active bool `json:"active,omitempty"` | |
| // Current indicates the current active revision if Active is | |
| // true or the last active revision if Active is false | |
| // (usually while a snap is being operated on or disabled) | |
| Current snap.Revision `json:"current"` | |
| Channel string `json:"channel,omitempty"` | |
| Flags | |
| // aliases, see aliasesv2.go | |
| Aliases map[string]*AliasTarget `json:"aliases,omitempty"` | |
| AutoAliasesDisabled bool `json:"auto-aliases-disabled,omitempty"` | |
| AliasesPending bool `json:"aliases-pending,omitempty"` | |
| // UserID of the user requesting the install | |
| UserID int `json:"user-id,omitempty"` | |
| } | |
| // Type returns the type of the snap or an error. | |
| // Should never error if Current is not nil. | |
| func (snapst *SnapState) Type() (snap.Type, error) { | |
| if snapst.SnapType == "" { | |
| return snap.Type(""), fmt.Errorf("snap type unset") | |
| } | |
| return snap.Type(snapst.SnapType), nil | |
| } | |
| // SetType records the type of the snap. | |
| func (snapst *SnapState) SetType(typ snap.Type) { | |
| snapst.SnapType = string(typ) | |
| } | |
| // IsInstalled returns whether the snap is installed, i.e. snapst represents an installed snap with Current revision set. | |
| func (snapst *SnapState) IsInstalled() bool { | |
| if snapst.Current.Unset() { | |
| if len(snapst.Sequence) > 0 { | |
| panic(fmt.Sprintf("snapst.Current and snapst.Sequence out of sync: %#v %#v", snapst.Current, snapst.Sequence)) | |
| } | |
| return false | |
| } | |
| return true | |
| } | |
| // LocalRevision returns the "latest" local revision. Local revisions | |
| // start at -1 and are counted down. | |
| func (snapst *SnapState) LocalRevision() snap.Revision { | |
| var local snap.Revision | |
| for _, si := range snapst.Sequence { | |
| if si.Revision.Local() && si.Revision.N < local.N { | |
| local = si.Revision | |
| } | |
| } | |
| return local | |
| } | |
| // CurrentSideInfo returns the side info for the revision indicated by snapst.Current in the snap revision sequence if there is one. | |
| func (snapst *SnapState) CurrentSideInfo() *snap.SideInfo { | |
| if !snapst.IsInstalled() { | |
| return nil | |
| } | |
| if idx := snapst.LastIndex(snapst.Current); idx >= 0 { | |
| return snapst.Sequence[idx] | |
| } | |
| panic("cannot find snapst.Current in the snapst.Sequence") | |
| } | |
| func (snapst *SnapState) previousSideInfo() *snap.SideInfo { | |
| n := len(snapst.Sequence) | |
| if n < 2 { | |
| return nil | |
| } | |
| // find "current" and return the one before that | |
| currentIndex := snapst.LastIndex(snapst.Current) | |
| if currentIndex <= 0 { | |
| return nil | |
| } | |
| return snapst.Sequence[currentIndex-1] | |
| } | |
| // LastIndex returns the last index of the given revision in the | |
| // snapst.Sequence | |
| func (snapst *SnapState) LastIndex(revision snap.Revision) int { | |
| for i := len(snapst.Sequence) - 1; i >= 0; i-- { | |
| if snapst.Sequence[i].Revision == revision { | |
| return i | |
| } | |
| } | |
| return -1 | |
| } | |
| // Block returns revisions that should be blocked on refreshes, | |
| // computed from Sequence[currentRevisionIndex+1:]. | |
| func (snapst *SnapState) Block() []snap.Revision { | |
| // return revisions from Sequence[currentIndex:] | |
| currentIndex := snapst.LastIndex(snapst.Current) | |
| if currentIndex < 0 || currentIndex+1 == len(snapst.Sequence) { | |
| return nil | |
| } | |
| out := make([]snap.Revision, len(snapst.Sequence)-currentIndex-1) | |
| for i, si := range snapst.Sequence[currentIndex+1:] { | |
| out[i] = si.Revision | |
| } | |
| return out | |
| } | |
| var ErrNoCurrent = errors.New("snap has no current revision") | |
| // Retrieval functions | |
| var readInfo = readInfoAnyway | |
| func readInfoAnyway(name string, si *snap.SideInfo) (*snap.Info, error) { | |
| info, err := snap.ReadInfo(name, si) | |
| if _, ok := err.(*snap.NotFoundError); ok { | |
| reason := fmt.Sprintf("cannot read snap %q: %s", name, err) | |
| info := &snap.Info{ | |
| SuggestedName: name, | |
| Broken: reason, | |
| } | |
| info.Apps = snap.GuessAppsForBroken(info) | |
| if si != nil { | |
| info.SideInfo = *si | |
| } | |
| return info, nil | |
| } | |
| return info, err | |
| } | |
| // CurrentInfo returns the information about the current active revision or the last active revision (if the snap is inactive). It returns the ErrNoCurrent error if snapst.Current is unset. | |
| func (snapst *SnapState) CurrentInfo() (*snap.Info, error) { | |
| cur := snapst.CurrentSideInfo() | |
| if cur == nil { | |
| return nil, ErrNoCurrent | |
| } | |
| return readInfo(cur.RealName, cur) | |
| } | |
| func revisionInSequence(snapst *SnapState, needle snap.Revision) bool { | |
| for _, si := range snapst.Sequence { | |
| if si.Revision == needle { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| type cachedStoreKey struct{} | |
| // ReplaceStore replaces the store used by the manager. | |
| func ReplaceStore(state *state.State, store StoreService) { | |
| state.Cache(cachedStoreKey{}, store) | |
| } | |
| func cachedStore(st *state.State) StoreService { | |
| ubuntuStore := st.Cached(cachedStoreKey{}) | |
| if ubuntuStore == nil { | |
| return nil | |
| } | |
| return ubuntuStore.(StoreService) | |
| } | |
| // the store implementation has the interface consumed here | |
| var _ StoreService = (*store.Store)(nil) | |
| // Store returns the store service used by the snapstate package. | |
| func Store(st *state.State) StoreService { | |
| if cachedStore := cachedStore(st); cachedStore != nil { | |
| return cachedStore | |
| } | |
| panic("internal error: needing the store before managers have initialized it") | |
| } | |
| // Manager returns a new snap manager. | |
| func Manager(st *state.State) (*SnapManager, error) { | |
| runner := state.NewTaskRunner(st) | |
| m := &SnapManager{ | |
| state: st, | |
| backend: backend.Backend{}, | |
| runner: runner, | |
| autoRefresh: newAutoRefresh(st), | |
| refreshHints: newRefreshHints(st), | |
| catalogRefresh: newCatalogRefresh(st), | |
| } | |
| if err := os.MkdirAll(dirs.SnapCookieDir, 0700); err != nil { | |
| return nil, fmt.Errorf("cannot create directory %q: %v", dirs.SnapCookieDir, err) | |
| } | |
| // this handler does nothing | |
| runner.AddHandler("nop", func(t *state.Task, _ *tomb.Tomb) error { | |
| return nil | |
| }, nil) | |
| // install/update related | |
| // TODO: no undo handler here, we may use the GC for this and just | |
| // remove anything that is not referenced anymore | |
| runner.AddHandler("prerequisites", m.doPrerequisites, nil) | |
| runner.AddHandler("prepare-snap", m.doPrepareSnap, m.undoPrepareSnap) | |
| runner.AddHandler("download-snap", m.doDownloadSnap, m.undoPrepareSnap) | |
| runner.AddHandler("mount-snap", m.doMountSnap, m.undoMountSnap) | |
| runner.AddHandler("unlink-current-snap", m.doUnlinkCurrentSnap, m.undoUnlinkCurrentSnap) | |
| runner.AddHandler("copy-snap-data", m.doCopySnapData, m.undoCopySnapData) | |
| runner.AddCleanup("copy-snap-data", m.cleanupCopySnapData) | |
| runner.AddHandler("link-snap", m.doLinkSnap, m.undoLinkSnap) | |
| runner.AddHandler("start-snap-services", m.startSnapServices, m.stopSnapServices) | |
| runner.AddHandler("switch-snap-channel", m.doSwitchSnapChannel, nil) | |
| runner.AddHandler("toggle-snap-flags", m.doToggleSnapFlags, nil) | |
| // FIXME: drop the task entirely after a while | |
| // (having this wart here avoids yet-another-patch) | |
| runner.AddHandler("cleanup", func(*state.Task, *tomb.Tomb) error { return nil }, nil) | |
| // remove related | |
| runner.AddHandler("stop-snap-services", m.stopSnapServices, m.startSnapServices) | |
| runner.AddHandler("unlink-snap", m.doUnlinkSnap, nil) | |
| runner.AddHandler("clear-snap", m.doClearSnapData, nil) | |
| runner.AddHandler("discard-snap", m.doDiscardSnap, nil) | |
| // alias related | |
| // FIXME: drop the task entirely after a while | |
| runner.AddHandler("clear-aliases", func(*state.Task, *tomb.Tomb) error { return nil }, nil) | |
| runner.AddHandler("set-auto-aliases", m.doSetAutoAliases, m.undoRefreshAliases) | |
| runner.AddHandler("setup-aliases", m.doSetupAliases, m.doRemoveAliases) | |
| runner.AddHandler("refresh-aliases", m.doRefreshAliases, m.undoRefreshAliases) | |
| runner.AddHandler("prune-auto-aliases", m.doPruneAutoAliases, m.undoRefreshAliases) | |
| runner.AddHandler("remove-aliases", m.doRemoveAliases, m.doSetupAliases) | |
| runner.AddHandler("alias", m.doAlias, m.undoRefreshAliases) | |
| runner.AddHandler("unalias", m.doUnalias, m.undoRefreshAliases) | |
| runner.AddHandler("disable-aliases", m.doDisableAliases, m.undoRefreshAliases) | |
| runner.AddHandler("prefer-aliases", m.doPreferAliases, m.undoRefreshAliases) | |
| // misc | |
| runner.AddHandler("switch-snap", m.doSwitchSnap, nil) | |
| // control serialisation | |
| runner.SetBlocked(m.blockedTask) | |
| writeSnapReadme() | |
| return m, nil | |
| } | |
| func (m *SnapManager) blockedTask(cand *state.Task, running []*state.Task) bool { | |
| // Serialize "prerequisites", the state lock is not enough as | |
| // Install() inside doPrerequisites() will unlock to talk to | |
| // the store. | |
| if cand.Kind() == "prerequisites" { | |
| for _, t := range running { | |
| if t.Kind() == "prerequisites" { | |
| return true | |
| } | |
| } | |
| } | |
| return false | |
| } | |
| // NextRefresh returns the time the next update of the system's snaps | |
| // will be attempted. | |
| // The caller should be holding the state lock. | |
| func (m *SnapManager) NextRefresh() time.Time { | |
| return m.autoRefresh.NextRefresh() | |
| } | |
| // LastRefresh returns the time the last snap update. | |
| // The caller should be holding the state lock. | |
| func (m *SnapManager) LastRefresh() (time.Time, error) { | |
| return m.autoRefresh.LastRefresh() | |
| } | |
| // RefreshSchedule returns the current refresh schedule as a string | |
| // suitable to display to a user. | |
| // The caller should be holding the state lock. | |
| func (m *SnapManager) RefreshSchedule() (string, error) { | |
| return m.autoRefresh.RefreshSchedule() | |
| } | |
| // ensureForceDevmodeDropsDevmodeFromState undoes the froced devmode | |
| // in snapstate for forced devmode distros. | |
| func (m *SnapManager) ensureForceDevmodeDropsDevmodeFromState() error { | |
| if !release.ReleaseInfo.ForceDevMode() { | |
| return nil | |
| } | |
| m.state.Lock() | |
| defer m.state.Unlock() | |
| // int because we might want to come back and do a second pass at cleanup | |
| var fixed int | |
| if err := m.state.Get("fix-forced-devmode", &fixed); err != nil && err != state.ErrNoState { | |
| return err | |
| } | |
| if fixed > 0 { | |
| return nil | |
| } | |
| for _, name := range []string{"core", "ubuntu-core"} { | |
| var snapst SnapState | |
| if err := Get(m.state, name, &snapst); err == state.ErrNoState { | |
| // nothing to see here | |
| continue | |
| } else if err != nil { | |
| // bad | |
| return err | |
| } | |
| if info := snapst.CurrentSideInfo(); info == nil || info.SnapID == "" { | |
| continue | |
| } | |
| snapst.DevMode = false | |
| Set(m.state, name, &snapst) | |
| } | |
| m.state.Set("fix-forced-devmode", 1) | |
| return nil | |
| } | |
| // ensureUbuntuCoreTransition will migrate systems that use "ubuntu-core" | |
| // to the new "core" snap | |
| func (m *SnapManager) ensureUbuntuCoreTransition() error { | |
| m.state.Lock() | |
| defer m.state.Unlock() | |
| var snapst SnapState | |
| err := Get(m.state, "ubuntu-core", &snapst) | |
| if err == state.ErrNoState { | |
| return nil | |
| } | |
| if err != nil && err != state.ErrNoState { | |
| return err | |
| } | |
| // check that there is no change in flight already, this is a | |
| // precaution to ensure the core transition is safe | |
| for _, chg := range m.state.Changes() { | |
| if !chg.Status().Ready() { | |
| // another change already in motion | |
| return nil | |
| } | |
| } | |
| // ensure we limit the retries in case something goes wrong | |
| var lastUbuntuCoreTransitionAttempt time.Time | |
| err = m.state.Get("ubuntu-core-transition-last-retry-time", &lastUbuntuCoreTransitionAttempt) | |
| if err != nil && err != state.ErrNoState { | |
| return err | |
| } | |
| now := time.Now() | |
| if !lastUbuntuCoreTransitionAttempt.IsZero() && lastUbuntuCoreTransitionAttempt.Add(6*time.Hour).After(now) { | |
| return nil | |
| } | |
| m.state.Set("ubuntu-core-transition-last-retry-time", now) | |
| var retryCount int | |
| err = m.state.Get("ubuntu-core-transition-retry", &retryCount) | |
| if err != nil && err != state.ErrNoState { | |
| return err | |
| } | |
| m.state.Set("ubuntu-core-transition-retry", retryCount+1) | |
| tss, err := TransitionCore(m.state, "ubuntu-core", "core") | |
| if err != nil { | |
| return err | |
| } | |
| msg := fmt.Sprintf(i18n.G("Transition ubuntu-core to core")) | |
| chg := m.state.NewChange("transition-ubuntu-core", msg) | |
| for _, ts := range tss { | |
| chg.AddAll(ts) | |
| } | |
| return nil | |
| } | |
| // Ensure implements StateManager.Ensure. | |
| func (m *SnapManager) Ensure() error { | |
| // do not exit right away on error | |
| errs := []error{ | |
| m.ensureAliasesV2(), | |
| m.ensureForceDevmodeDropsDevmodeFromState(), | |
| m.ensureUbuntuCoreTransition(), | |
| m.refreshHints.Ensure(), | |
| m.autoRefresh.Ensure(), | |
| m.catalogRefresh.Ensure(), | |
| } | |
| m.runner.Ensure() | |
| //FIXME: use firstErr helper | |
| for _, e := range errs { | |
| if e != nil { | |
| return e | |
| } | |
| } | |
| return nil | |
| } | |
| func (m *SnapManager) KnownTaskKinds() []string { | |
| return m.runner.KnownTaskKinds() | |
| } | |
| // Wait implements StateManager.Wait. | |
| func (m *SnapManager) Wait() { | |
| m.runner.Wait() | |
| } | |
| // Stop implements StateManager.Stop. | |
| func (m *SnapManager) Stop() { | |
| m.runner.Stop() | |
| } |