github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/overlord/snapstate/refresh_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019 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_test
    21  
    22  import (
    23  	"io/ioutil"
    24  	"os"
    25  	"path/filepath"
    26  
    27  	. "gopkg.in/check.v1"
    28  
    29  	"github.com/snapcore/snapd/cmd/snaplock/runinhibit"
    30  	"github.com/snapcore/snapd/dirs"
    31  	"github.com/snapcore/snapd/overlord/snapstate"
    32  	"github.com/snapcore/snapd/overlord/state"
    33  	"github.com/snapcore/snapd/snap"
    34  	"github.com/snapcore/snapd/snap/snaptest"
    35  	"github.com/snapcore/snapd/testutil"
    36  )
    37  
    38  type refreshSuite struct {
    39  	testutil.BaseTest
    40  	state *state.State
    41  	info  *snap.Info
    42  	pids  map[string][]int
    43  }
    44  
    45  var _ = Suite(&refreshSuite{})
    46  
    47  func (s *refreshSuite) SetUpTest(c *C) {
    48  	dirs.SetRootDir(c.MkDir())
    49  	yamlText := `
    50  name: pkg
    51  version: 1
    52  apps:
    53    daemon:
    54      command: dummy
    55      daemon: simple
    56    app:
    57      command: dummy
    58  hooks:
    59    configure:
    60  `
    61  	s.info = snaptest.MockInfo(c, yamlText, nil)
    62  	s.pids = nil
    63  	restore := snapstate.MockPidsOfSnap(func(instanceName string) (map[string][]int, error) {
    64  		c.Assert(instanceName, Equals, s.info.InstanceName())
    65  		return s.pids, nil
    66  	})
    67  	s.AddCleanup(restore)
    68  	s.AddCleanup(func() { dirs.SetRootDir("") })
    69  	s.state = state.New(nil)
    70  }
    71  
    72  func (s *refreshSuite) TestSoftNothingRunningRefreshCheck(c *C) {
    73  	// Services are not blocking soft refresh check,
    74  	// they will be stopped before refresh.
    75  	s.pids = map[string][]int{
    76  		"snap.pkg.daemon": {100},
    77  	}
    78  	err := snapstate.SoftNothingRunningRefreshCheck(s.info)
    79  	c.Check(err, IsNil)
    80  
    81  	// Apps are blocking soft refresh check. They are not stopped by
    82  	// snapd, unless the app is running for longer than the maximum
    83  	// duration allowed for postponing refreshes.
    84  	s.pids = map[string][]int{
    85  		"snap.pkg.daemon": {100},
    86  		"snap.pkg.app":    {101},
    87  	}
    88  	err = snapstate.SoftNothingRunningRefreshCheck(s.info)
    89  	c.Assert(err, NotNil)
    90  	c.Check(err.Error(), Equals, `snap "pkg" has running apps (app)`)
    91  	c.Check(err.(*snapstate.BusySnapError).Pids(), DeepEquals, []int{101})
    92  
    93  	// Hooks behave just like apps. IDEA: perhaps hooks should not be
    94  	// killed this way? They have their own life-cycle management.
    95  	s.pids = map[string][]int{
    96  		"snap.pkg.hook.configure": {105},
    97  	}
    98  	err = snapstate.SoftNothingRunningRefreshCheck(s.info)
    99  	c.Assert(err, NotNil)
   100  	c.Check(err.Error(), Equals, `snap "pkg" has running hooks (configure)`)
   101  	c.Check(err.(*snapstate.BusySnapError).Pids(), DeepEquals, []int{105})
   102  
   103  	// Both apps and hooks can be running.
   104  	s.pids = map[string][]int{
   105  		"snap.pkg.hook.configure": {105},
   106  		"snap.pkg.app":            {106},
   107  	}
   108  	err = snapstate.SoftNothingRunningRefreshCheck(s.info)
   109  	c.Assert(err, NotNil)
   110  	c.Check(err.Error(), Equals, `snap "pkg" has running apps (app) and hooks (configure)`)
   111  	c.Check(err.(*snapstate.BusySnapError).Pids(), DeepEquals, []int{105, 106})
   112  }
   113  
   114  func (s *refreshSuite) TestHardNothingRunningRefreshCheck(c *C) {
   115  	// Regular services are blocking hard refresh check.
   116  	// We were expecting them to be gone by now.
   117  	s.pids = map[string][]int{
   118  		"snap.pkg.daemon": {100},
   119  	}
   120  	err := snapstate.HardNothingRunningRefreshCheck(s.info)
   121  	c.Assert(err, NotNil)
   122  	c.Check(err.Error(), Equals, `snap "pkg" has running apps (daemon)`)
   123  	c.Check(err.(*snapstate.BusySnapError).Pids(), DeepEquals, []int{100})
   124  
   125  	// When the service is supposed to endure refreshes it will not be
   126  	// stopped. As such such services cannot block refresh.
   127  	s.info.Apps["daemon"].RefreshMode = "endure"
   128  	err = snapstate.HardNothingRunningRefreshCheck(s.info)
   129  	c.Check(err, IsNil)
   130  	s.info.Apps["daemon"].RefreshMode = ""
   131  
   132  	// Applications are also blocking hard refresh check.
   133  	s.pids = map[string][]int{
   134  		"snap.pkg.app": {101},
   135  	}
   136  	err = snapstate.HardNothingRunningRefreshCheck(s.info)
   137  	c.Assert(err, NotNil)
   138  	c.Check(err.Error(), Equals, `snap "pkg" has running apps (app)`)
   139  	c.Assert(err, FitsTypeOf, &snapstate.BusySnapError{})
   140  	c.Check(err.(*snapstate.BusySnapError).Pids(), DeepEquals, []int{101})
   141  
   142  	// Hooks are equally blocking hard refresh check.
   143  	s.pids = map[string][]int{
   144  		"snap.pkg.hook.configure": {105},
   145  	}
   146  	err = snapstate.HardNothingRunningRefreshCheck(s.info)
   147  	c.Assert(err, NotNil)
   148  	c.Check(err.Error(), Equals, `snap "pkg" has running hooks (configure)`)
   149  	c.Check(err.(*snapstate.BusySnapError).Pids(), DeepEquals, []int{105})
   150  }
   151  
   152  func (s *refreshSuite) TestPendingSnapRefreshInfo(c *C) {
   153  	err := snapstate.NewBusySnapError(s.info, nil, nil, nil)
   154  	refreshInfo := err.PendingSnapRefreshInfo()
   155  	c.Check(refreshInfo.InstanceName, Equals, s.info.InstanceName())
   156  	// The information about a busy app is not populated because
   157  	// the original error did not have the corresponding information.
   158  	c.Check(refreshInfo.BusyAppName, Equals, "")
   159  	c.Check(refreshInfo.BusyAppDesktopEntry, Equals, "")
   160  
   161  	// If we create a matching desktop entry then relevant meta-data is added.
   162  	err = snapstate.NewBusySnapError(s.info, nil, []string{"app"}, nil)
   163  	desktopFile := s.info.Apps["app"].DesktopFile()
   164  	c.Assert(os.MkdirAll(filepath.Dir(desktopFile), 0755), IsNil)
   165  	c.Assert(ioutil.WriteFile(desktopFile, nil, 0644), IsNil)
   166  	refreshInfo = err.PendingSnapRefreshInfo()
   167  	c.Check(refreshInfo.InstanceName, Equals, s.info.InstanceName())
   168  	c.Check(refreshInfo.BusyAppName, Equals, "app")
   169  	c.Check(refreshInfo.BusyAppDesktopEntry, Equals, "pkg_app")
   170  }
   171  
   172  func (s *refreshSuite) addInstalledSnap(snapst *snapstate.SnapState) (*snapstate.SnapState, *snap.Info) {
   173  	snapName := snapst.Sequence[0].RealName
   174  	snapstate.Set(s.state, snapName, snapst)
   175  	info := &snap.Info{SideInfo: snap.SideInfo{RealName: snapName, Revision: snapst.Current}}
   176  	return snapst, info
   177  }
   178  
   179  func (s *refreshSuite) addDummyInstalledSnap() (*snapstate.SnapState, *snap.Info) {
   180  	return s.addInstalledSnap(&snapstate.SnapState{
   181  		Active: true,
   182  		Sequence: []*snap.SideInfo{
   183  			{RealName: "pkg", Revision: snap.R(5), SnapID: "pkg-snap-id"},
   184  		},
   185  		Current:  snap.R(5),
   186  		SnapType: "app",
   187  		UserID:   1,
   188  	})
   189  }
   190  
   191  func (s *refreshSuite) TestDoSoftRefreshCheckAllowed(c *C) {
   192  	// Pretend we have a snap
   193  	s.state.Lock()
   194  	defer s.state.Unlock()
   195  	snapst, info := s.addDummyInstalledSnap()
   196  
   197  	// Pretend that snaps can refresh normally.
   198  	restore := snapstate.MockGenericRefreshCheck(func(info *snap.Info, canAppRunDuringRefresh func(app *snap.AppInfo) bool) error {
   199  		return nil
   200  	})
   201  	defer restore()
   202  
   203  	// Soft refresh should not fail.
   204  	err := snapstate.SoftCheckNothingRunningForRefresh(s.state, snapst, info)
   205  	c.Assert(err, IsNil)
   206  
   207  	// In addition, the inhibition lock is not set.
   208  	hint, err := runinhibit.IsLocked(info.InstanceName())
   209  	c.Assert(err, IsNil)
   210  	c.Check(hint, Equals, runinhibit.HintNotInhibited)
   211  }
   212  
   213  func (s *refreshSuite) TestDoSoftRefreshCheckDisallowed(c *C) {
   214  	// Pretend we have a snap
   215  	s.state.Lock()
   216  	defer s.state.Unlock()
   217  	snapst, info := s.addDummyInstalledSnap()
   218  
   219  	// Pretend that snaps cannot refresh.
   220  	restore := snapstate.MockGenericRefreshCheck(func(info *snap.Info, canAppRunDuringRefresh func(app *snap.AppInfo) bool) error {
   221  		return &snapstate.BusySnapError{SnapInfo: info}
   222  	})
   223  	defer restore()
   224  
   225  	// Soft refresh should fail with a proper error.
   226  	err := snapstate.SoftCheckNothingRunningForRefresh(s.state, snapst, info)
   227  	c.Assert(err, ErrorMatches, `snap "pkg" has running apps or hooks`)
   228  
   229  	// Sanity check: the inhibition lock was not set.
   230  	hint, err := runinhibit.IsLocked(info.InstanceName())
   231  	c.Assert(err, IsNil)
   232  	c.Check(hint, Equals, runinhibit.HintNotInhibited)
   233  }
   234  
   235  func (s *refreshSuite) TestDoHardRefreshFlowRefreshAllowed(c *C) {
   236  	backend := &fakeSnappyBackend{}
   237  	// Pretend we have a snap
   238  	s.state.Lock()
   239  	defer s.state.Unlock()
   240  	snapst, info := s.addDummyInstalledSnap()
   241  
   242  	// Pretend that snaps can refresh normally.
   243  	restore := snapstate.MockGenericRefreshCheck(func(info *snap.Info, canAppRunDuringRefresh func(app *snap.AppInfo) bool) error {
   244  		return nil
   245  	})
   246  	defer restore()
   247  
   248  	// Hard refresh should not fail and return a valid lock.
   249  	lock, err := snapstate.HardEnsureNothingRunningDuringRefresh(backend, s.state, snapst, info)
   250  	c.Assert(err, IsNil)
   251  	c.Assert(lock, NotNil)
   252  	defer lock.Close()
   253  
   254  	// We should be able to unlock the lock without an error because
   255  	// it was acquired in the same process by the tested logic.
   256  	c.Assert(lock.Unlock(), IsNil)
   257  
   258  	// In addition, the fake backend recorded that a lock was established.
   259  	op := backend.ops.MustFindOp(c, "run-inhibit-snap-for-unlink")
   260  	c.Check(op.inhibitHint, Equals, runinhibit.Hint("refresh"))
   261  }
   262  
   263  func (s *refreshSuite) TestDoHardRefreshFlowRefreshDisallowed(c *C) {
   264  	backend := &fakeSnappyBackend{}
   265  	// Pretend we have a snap
   266  	s.state.Lock()
   267  	defer s.state.Unlock()
   268  	snapst, info := s.addDummyInstalledSnap()
   269  
   270  	// Pretend that snaps cannot refresh.
   271  	restore := snapstate.MockGenericRefreshCheck(func(info *snap.Info, canAppRunDuringRefresh func(app *snap.AppInfo) bool) error {
   272  		return &snapstate.BusySnapError{SnapInfo: info}
   273  	})
   274  	defer restore()
   275  
   276  	// Hard refresh should fail and not return a lock.
   277  	lock, err := snapstate.HardEnsureNothingRunningDuringRefresh(backend, s.state, snapst, info)
   278  	c.Assert(err, ErrorMatches, `snap "pkg" has running apps or hooks`)
   279  	c.Assert(lock, IsNil)
   280  
   281  	// Sanity check: the inhibition lock was not set.
   282  	op := backend.ops.MustFindOp(c, "run-inhibit-snap-for-unlink")
   283  	c.Check(op.inhibitHint, Equals, runinhibit.Hint("refresh"))
   284  }