github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/overlord/snapstate/catalogrefresh.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 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 snapstate
    21  
    22  import (
    23  	"errors"
    24  	"fmt"
    25  	"os"
    26  	"sort"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/snapcore/snapd/advisor"
    31  	"github.com/snapcore/snapd/dirs"
    32  	"github.com/snapcore/snapd/logger"
    33  	"github.com/snapcore/snapd/osutil"
    34  	"github.com/snapcore/snapd/overlord/auth"
    35  	"github.com/snapcore/snapd/overlord/state"
    36  	"github.com/snapcore/snapd/randutil"
    37  	"github.com/snapcore/snapd/snapdenv"
    38  	"github.com/snapcore/snapd/store"
    39  	"github.com/snapcore/snapd/timings"
    40  )
    41  
    42  var (
    43  	catalogRefreshDelayBase      = 24 * time.Hour
    44  	catalogRefreshDelayWithDelta = 24*time.Hour + 1 + randutil.RandomDuration(6*time.Hour)
    45  )
    46  
    47  type catalogRefresh struct {
    48  	state *state.State
    49  
    50  	nextCatalogRefresh time.Time
    51  }
    52  
    53  func newCatalogRefresh(st *state.State) *catalogRefresh {
    54  	return &catalogRefresh{state: st}
    55  }
    56  
    57  // Ensure will ensure that the catalog refresh happens
    58  func (r *catalogRefresh) Ensure() error {
    59  	r.state.Lock()
    60  	defer r.state.Unlock()
    61  
    62  	// sneakily don't do anything if in testing
    63  	if CanAutoRefresh == nil {
    64  		return nil
    65  	}
    66  
    67  	// if system is not seeded yet, it is first boot situation
    68  	// do not bother refreshing catalog, snap list is empty anyway
    69  	// beside there is high change device has no internet
    70  	var seeded bool
    71  	err := r.state.Get("seeded", &seeded)
    72  	if err == state.ErrNoState || !seeded {
    73  		logger.Debugf("CatalogRefresh:Ensure: skipping refresh, system is not seeded yet")
    74  		// not seeded yet
    75  		return nil
    76  	}
    77  
    78  	// similar to the not yet seeded case, on uc20 install mode it doesn't make
    79  	// sense to refresh the catalog for an ephemeral system
    80  	deviceCtx, err := DeviceCtx(r.state, nil, nil)
    81  	if err != nil {
    82  		// if we are seeded we should have a device context
    83  		return err
    84  	}
    85  
    86  	if deviceCtx.SystemMode() == "install" {
    87  		// skip the refresh
    88  		return nil
    89  	}
    90  
    91  	now := time.Now()
    92  	delay := catalogRefreshDelayBase
    93  	if r.nextCatalogRefresh.IsZero() {
    94  		// try to use the timestamp on the sections file
    95  		if st, err := os.Stat(dirs.SnapNamesFile); err == nil && st.ModTime().Before(now) {
    96  			// add the delay with the delta so we spread the load a bit
    97  			r.nextCatalogRefresh = st.ModTime().Add(catalogRefreshDelayWithDelta)
    98  		} else {
    99  			// first time scheduling, add the delta
   100  			delay = catalogRefreshDelayWithDelta
   101  		}
   102  	}
   103  
   104  	theStore := Store(r.state, nil)
   105  	needsRefresh := r.nextCatalogRefresh.IsZero() || r.nextCatalogRefresh.Before(now)
   106  
   107  	if !needsRefresh {
   108  		return nil
   109  	}
   110  
   111  	next := now.Add(delay)
   112  	// catalog refresh does not carry on trying on error
   113  	r.nextCatalogRefresh = next
   114  
   115  	logger.Debugf("Catalog refresh starting now; next scheduled for %s.", next)
   116  
   117  	err = refreshCatalogs(r.state, theStore)
   118  	switch err {
   119  	case nil:
   120  		logger.Debugf("Catalog refresh succeeded.")
   121  	case store.ErrTooManyRequests:
   122  		logger.Debugf("Catalog refresh postponed.")
   123  		err = nil
   124  	case errSkipCatalogRefreshWhenTesting:
   125  		logger.Debugf("Catalog refresh skipped when testing is enabled")
   126  		err = nil
   127  	default:
   128  		logger.Debugf("Catalog refresh failed: %v.", err)
   129  	}
   130  	return err
   131  }
   132  
   133  var newCmdDB = advisor.Create
   134  
   135  var errSkipCatalogRefreshWhenTesting = errors.New("skipping when testing is enabled")
   136  
   137  func refreshCatalogs(st *state.State, theStore StoreService) error {
   138  	if snapdenv.Testing() && !osutil.GetenvBool("SNAPD_CATALOG_REFRESH") {
   139  		// with snapd testing enabled, SNAPD_CATALOG_REFRESH is gating
   140  		// the catalog refresh
   141  		return errSkipCatalogRefreshWhenTesting
   142  	}
   143  
   144  	st.Unlock()
   145  	defer st.Lock()
   146  
   147  	perfTimings := timings.New(map[string]string{"ensure": "refresh-catalogs"})
   148  
   149  	if err := os.MkdirAll(dirs.SnapCacheDir, 0755); err != nil {
   150  		return fmt.Errorf("cannot create directory %q: %v", dirs.SnapCacheDir, err)
   151  	}
   152  
   153  	var sections []string
   154  	var err error
   155  	timings.Run(perfTimings, "get-sections", "query store for sections", func(tm timings.Measurer) {
   156  		sections, err = theStore.Sections(auth.EnsureContextTODO(), nil)
   157  	})
   158  	if err != nil {
   159  		return err
   160  	}
   161  	sort.Strings(sections)
   162  	if err := osutil.AtomicWriteFile(dirs.SnapSectionsFile, []byte(strings.Join(sections, "\n")), 0644, 0); err != nil {
   163  		return err
   164  	}
   165  
   166  	namesFile, err := osutil.NewAtomicFile(dirs.SnapNamesFile, 0644, 0, osutil.NoChown, osutil.NoChown)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	defer namesFile.Cancel()
   171  
   172  	cmdDB, err := newCmdDB()
   173  	if err != nil {
   174  		return err
   175  	}
   176  
   177  	// if all goes well we'll Commit() making this a NOP:
   178  	defer cmdDB.Rollback()
   179  
   180  	timings.Run(perfTimings, "write-catalogs", "query store for catalogs", func(tm timings.Measurer) {
   181  		err = theStore.WriteCatalogs(auth.EnsureContextTODO(), namesFile, cmdDB)
   182  	})
   183  	if err != nil {
   184  		return err
   185  	}
   186  
   187  	err1 := namesFile.Commit()
   188  	err2 := cmdDB.Commit()
   189  
   190  	if err2 != nil {
   191  		return err2
   192  	}
   193  
   194  	st.Lock()
   195  	perfTimings.Save(st)
   196  	st.Unlock()
   197  
   198  	return err1
   199  }