github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/daemon/api_base_test.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 daemon_test
    21  
    22  import (
    23  	"context"
    24  	"crypto"
    25  	"fmt"
    26  	"io/ioutil"
    27  	"net/http"
    28  	"os"
    29  	"path/filepath"
    30  	"time"
    31  
    32  	"github.com/gorilla/mux"
    33  	"golang.org/x/crypto/sha3"
    34  	"gopkg.in/check.v1"
    35  	"gopkg.in/tomb.v2"
    36  
    37  	"github.com/snapcore/snapd/asserts"
    38  	"github.com/snapcore/snapd/asserts/assertstest"
    39  	"github.com/snapcore/snapd/asserts/sysdb"
    40  	"github.com/snapcore/snapd/daemon"
    41  	"github.com/snapcore/snapd/dirs"
    42  	"github.com/snapcore/snapd/osutil"
    43  	"github.com/snapcore/snapd/overlord"
    44  	"github.com/snapcore/snapd/overlord/assertstate"
    45  	"github.com/snapcore/snapd/overlord/assertstate/assertstatetest"
    46  	"github.com/snapcore/snapd/overlord/auth"
    47  	"github.com/snapcore/snapd/overlord/devicestate"
    48  	"github.com/snapcore/snapd/overlord/devicestate/devicestatetest"
    49  	"github.com/snapcore/snapd/overlord/ifacestate"
    50  	"github.com/snapcore/snapd/overlord/snapstate"
    51  	"github.com/snapcore/snapd/overlord/state"
    52  	"github.com/snapcore/snapd/sandbox"
    53  	"github.com/snapcore/snapd/snap"
    54  	"github.com/snapcore/snapd/snap/snaptest"
    55  	"github.com/snapcore/snapd/store"
    56  	"github.com/snapcore/snapd/store/storetest"
    57  	"github.com/snapcore/snapd/systemd"
    58  	"github.com/snapcore/snapd/testutil"
    59  )
    60  
    61  // TODO: as we split api_test.go and move more tests to live in daemon_test
    62  // instead of daemon, split out functionality from APIBaseSuite
    63  // to only the relevant suite when possible
    64  
    65  type apiBaseSuite struct {
    66  	testutil.BaseTest
    67  
    68  	storetest.Store
    69  
    70  	rsnaps            []*snap.Info
    71  	err               error
    72  	vars              map[string]string
    73  	storeSearch       store.Search
    74  	suggestedCurrency string
    75  	d                 *daemon.Daemon
    76  	user              *auth.UserState
    77  	ctx               context.Context
    78  	currentSnaps      []*store.CurrentSnap
    79  	actions           []*store.SnapAction
    80  
    81  	restoreRelease func()
    82  
    83  	StoreSigning *assertstest.StoreStack
    84  	Brands       *assertstest.SigningAccounts
    85  
    86  	systemctlRestorer func()
    87  	SysctlBufs        [][]byte
    88  
    89  	connectivityResult map[string]bool
    90  
    91  	restoreSanitize func()
    92  	restoreMuxVars  func()
    93  }
    94  
    95  func (s *apiBaseSuite) pokeStateLock() {
    96  	// the store should be called without the state lock held. Try
    97  	// to acquire it.
    98  	st := s.d.Overlord().State()
    99  	st.Lock()
   100  	st.Unlock()
   101  }
   102  
   103  func (s *apiBaseSuite) SnapInfo(ctx context.Context, spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) {
   104  	s.pokeStateLock()
   105  	s.user = user
   106  	s.ctx = ctx
   107  	if len(s.rsnaps) > 0 {
   108  		return s.rsnaps[0], s.err
   109  	}
   110  	return nil, s.err
   111  }
   112  
   113  func (s *apiBaseSuite) Find(ctx context.Context, search *store.Search, user *auth.UserState) ([]*snap.Info, error) {
   114  	s.pokeStateLock()
   115  
   116  	s.storeSearch = *search
   117  	s.user = user
   118  	s.ctx = ctx
   119  
   120  	return s.rsnaps, s.err
   121  }
   122  
   123  func (s *apiBaseSuite) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) {
   124  	s.pokeStateLock()
   125  	if assertQuery != nil {
   126  		toResolve, toResolveSeq, err := assertQuery.ToResolve()
   127  		if err != nil {
   128  			return nil, nil, err
   129  		}
   130  		if len(toResolve) != 0 || len(toResolveSeq) != 0 {
   131  			panic("no assertion query support")
   132  		}
   133  	}
   134  
   135  	if ctx == nil {
   136  		panic("context required")
   137  	}
   138  	s.currentSnaps = currentSnaps
   139  	s.actions = actions
   140  	s.user = user
   141  
   142  	sars := make([]store.SnapActionResult, len(s.rsnaps))
   143  	for i, rsnap := range s.rsnaps {
   144  		sars[i] = store.SnapActionResult{Info: rsnap}
   145  	}
   146  	return sars, nil, s.err
   147  }
   148  
   149  func (s *apiBaseSuite) SuggestedCurrency() string {
   150  	s.pokeStateLock()
   151  
   152  	return s.suggestedCurrency
   153  }
   154  
   155  func (s *apiBaseSuite) ConnectivityCheck() (map[string]bool, error) {
   156  	s.pokeStateLock()
   157  
   158  	return s.connectivityResult, s.err
   159  }
   160  
   161  func (s *apiBaseSuite) muxVars(*http.Request) map[string]string {
   162  	return s.vars
   163  }
   164  
   165  func (s *apiBaseSuite) SetUpSuite(c *check.C) {
   166  	s.restoreMuxVars = daemon.MockMuxVars(s.muxVars)
   167  	s.restoreRelease = sandbox.MockForceDevMode(false)
   168  	s.systemctlRestorer = systemd.MockSystemctl(s.systemctl)
   169  	s.restoreSanitize = snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})
   170  }
   171  
   172  func (s *apiBaseSuite) TearDownSuite(c *check.C) {
   173  	s.restoreMuxVars()
   174  	s.restoreRelease()
   175  	s.systemctlRestorer()
   176  	s.restoreSanitize()
   177  }
   178  
   179  func (s *apiBaseSuite) systemctl(args ...string) (buf []byte, err error) {
   180  	if len(s.SysctlBufs) > 0 {
   181  		buf, s.SysctlBufs = s.SysctlBufs[0], s.SysctlBufs[1:]
   182  	}
   183  	return buf, err
   184  }
   185  
   186  var (
   187  	brandPrivKey, _ = assertstest.GenerateKey(752)
   188  )
   189  
   190  func (s *apiBaseSuite) SetUpTest(c *check.C) {
   191  	s.BaseTest.SetUpTest(c)
   192  
   193  	ctlcmds := testutil.MockCommand(c, "systemctl", "").Also("journalctl", "")
   194  	s.AddCleanup(ctlcmds.Restore)
   195  
   196  	s.SysctlBufs = nil
   197  
   198  	dirs.SetRootDir(c.MkDir())
   199  	err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755)
   200  	restore := osutil.MockMountInfo("")
   201  	s.AddCleanup(restore)
   202  
   203  	c.Assert(err, check.IsNil)
   204  	c.Assert(os.MkdirAll(dirs.SnapMountDir, 0755), check.IsNil)
   205  	c.Assert(os.MkdirAll(dirs.SnapBlobDir, 0755), check.IsNil)
   206  
   207  	s.rsnaps = nil
   208  	s.suggestedCurrency = ""
   209  	s.storeSearch = store.Search{}
   210  	s.err = nil
   211  	s.vars = nil
   212  	s.user = nil
   213  	s.d = nil
   214  	s.currentSnaps = nil
   215  	s.actions = nil
   216  	// Disable real security backends for all API tests
   217  	s.AddCleanup(ifacestate.MockSecurityBackends(nil))
   218  
   219  	s.StoreSigning = assertstest.NewStoreStack("can0nical", nil)
   220  	s.AddCleanup(sysdb.InjectTrusted(s.StoreSigning.Trusted))
   221  
   222  	s.Brands = assertstest.NewSigningAccounts(s.StoreSigning)
   223  	s.Brands.Register("my-brand", brandPrivKey, nil)
   224  }
   225  
   226  func (s *apiBaseSuite) TearDownTest(c *check.C) {
   227  	s.d = nil
   228  	s.ctx = nil
   229  
   230  	dirs.SetRootDir("")
   231  	s.BaseTest.TearDownTest(c)
   232  }
   233  
   234  func (s *apiBaseSuite) mockModel(c *check.C, st *state.State, model *asserts.Model) {
   235  	// realistic model setup
   236  	if model == nil {
   237  		model = s.Brands.Model("can0nical", "pc", map[string]interface{}{
   238  			"architecture": "amd64",
   239  			"gadget":       "gadget",
   240  			"kernel":       "kernel",
   241  		})
   242  	}
   243  
   244  	snapstate.DeviceCtx = devicestate.DeviceCtx
   245  
   246  	assertstatetest.AddMany(st, model)
   247  
   248  	devicestatetest.SetDevice(st, &auth.DeviceState{
   249  		Brand:  model.BrandID(),
   250  		Model:  model.Model(),
   251  		Serial: "serialserial",
   252  	})
   253  }
   254  
   255  func (s *apiBaseSuite) daemonWithStore(c *check.C, sto snapstate.StoreService) *daemon.Daemon {
   256  	if s.d != nil {
   257  		panic("called daemon*() twice")
   258  	}
   259  	d, err := daemon.NewAndAddRoutes()
   260  	c.Assert(err, check.IsNil)
   261  
   262  	st := d.Overlord().State()
   263  	// mark as already seeded
   264  	st.Lock()
   265  	st.Set("seeded", true)
   266  	st.Unlock()
   267  	c.Assert(d.Overlord().StartUp(), check.IsNil)
   268  
   269  	st.Lock()
   270  	defer st.Unlock()
   271  	snapstate.ReplaceStore(st, sto)
   272  	// registered
   273  	s.mockModel(c, st, nil)
   274  
   275  	// don't actually try to talk to the store on snapstate.Ensure
   276  	// needs doing after the call to devicestate.Manager (which
   277  	// happens in daemon.New via overlord.New)
   278  	snapstate.CanAutoRefresh = nil
   279  
   280  	s.d = d
   281  	return d
   282  }
   283  
   284  func (s *apiBaseSuite) resetDaemon() {
   285  	s.d = nil
   286  }
   287  
   288  func (s *apiBaseSuite) daemon(c *check.C) *daemon.Daemon {
   289  	return s.daemonWithStore(c, s)
   290  }
   291  
   292  func (s *apiBaseSuite) daemonWithOverlordMock(c *check.C) *daemon.Daemon {
   293  	if s.d != nil {
   294  		panic("called daemon*() twice")
   295  	}
   296  
   297  	o := overlord.Mock()
   298  	s.d = daemon.NewWithOverlord(o)
   299  	return s.d
   300  }
   301  
   302  func (s *apiBaseSuite) daemonWithOverlordMockAndStore(c *check.C) *daemon.Daemon {
   303  	if s.d != nil {
   304  		panic("called daemon*() twice")
   305  	}
   306  
   307  	o := overlord.Mock()
   308  	d := daemon.NewWithOverlord(o)
   309  
   310  	st := d.Overlord().State()
   311  	// adds an assertion db
   312  	assertstate.Manager(st, o.TaskRunner())
   313  	st.Lock()
   314  	defer st.Unlock()
   315  	snapstate.ReplaceStore(st, s)
   316  
   317  	s.d = d
   318  	return d
   319  }
   320  
   321  type fakeSnapManager struct{}
   322  
   323  func newFakeSnapManager(st *state.State, runner *state.TaskRunner) *fakeSnapManager {
   324  	runner.AddHandler("fake-install-snap", func(t *state.Task, _ *tomb.Tomb) error {
   325  		return nil
   326  	}, nil)
   327  	runner.AddHandler("fake-install-snap-error", func(t *state.Task, _ *tomb.Tomb) error {
   328  		return fmt.Errorf("fake-install-snap-error errored")
   329  	}, nil)
   330  
   331  	return &fakeSnapManager{}
   332  }
   333  
   334  func (m *fakeSnapManager) Ensure() error {
   335  	return nil
   336  }
   337  
   338  // sanity
   339  var _ overlord.StateManager = (*fakeSnapManager)(nil)
   340  
   341  func (s *apiBaseSuite) daemonWithFakeSnapManager(c *check.C) *daemon.Daemon {
   342  	d := s.daemonWithOverlordMockAndStore(c)
   343  	st := d.Overlord().State()
   344  	runner := d.Overlord().TaskRunner()
   345  	d.Overlord().AddManager(newFakeSnapManager(st, runner))
   346  	d.Overlord().AddManager(runner)
   347  	c.Assert(d.Overlord().StartUp(), check.IsNil)
   348  	return d
   349  }
   350  
   351  func (s *apiBaseSuite) waitTrivialChange(c *check.C, chg *state.Change) {
   352  	err := s.d.Overlord().Settle(5 * time.Second)
   353  	c.Assert(err, check.IsNil)
   354  	c.Assert(chg.IsReady(), check.Equals, true)
   355  }
   356  
   357  func (s *apiBaseSuite) mkInstalledDesktopFile(c *check.C, name, content string) string {
   358  	df := filepath.Join(dirs.SnapDesktopFilesDir, name)
   359  	err := os.MkdirAll(filepath.Dir(df), 0755)
   360  	c.Assert(err, check.IsNil)
   361  	err = ioutil.WriteFile(df, []byte(content), 0644)
   362  	c.Assert(err, check.IsNil)
   363  	return df
   364  }
   365  
   366  func (s *apiBaseSuite) mockSnap(c *check.C, yamlText string) *snap.Info {
   367  	if s.d == nil {
   368  		panic("call s.daemon(c) etc in your test first")
   369  	}
   370  
   371  	snapInfo := snaptest.MockSnap(c, yamlText, &snap.SideInfo{Revision: snap.R(1)})
   372  
   373  	st := s.d.Overlord().State()
   374  
   375  	st.Lock()
   376  	defer st.Unlock()
   377  
   378  	// Put a side info into the state
   379  	snapstate.Set(st, snapInfo.InstanceName(), &snapstate.SnapState{
   380  		Active: true,
   381  		Sequence: []*snap.SideInfo{
   382  			{
   383  				RealName: snapInfo.SnapName(),
   384  				Revision: snapInfo.Revision,
   385  				SnapID:   "ididid",
   386  			},
   387  		},
   388  		Current:  snapInfo.Revision,
   389  		SnapType: string(snapInfo.Type()),
   390  	})
   391  
   392  	// Put the snap into the interface repository
   393  	repo := s.d.Overlord().InterfaceManager().Repository()
   394  	err := repo.AddSnap(snapInfo)
   395  	c.Assert(err, check.IsNil)
   396  	return snapInfo
   397  }
   398  
   399  func (s *apiBaseSuite) mkInstalledInState(c *check.C, d *daemon.Daemon, instanceName, developer, version string, revision snap.Revision, active bool, extraYaml string) *snap.Info {
   400  	snapName, instanceKey := snap.SplitInstanceName(instanceName)
   401  
   402  	if revision.Local() && developer != "" {
   403  		panic("not supported")
   404  	}
   405  
   406  	var snapID string
   407  	if revision.Store() {
   408  		snapID = snapName + "-id"
   409  	}
   410  	// Collect arguments into a snap.SideInfo structure
   411  	sideInfo := &snap.SideInfo{
   412  		SnapID:   snapID,
   413  		RealName: snapName,
   414  		Revision: revision,
   415  		Channel:  "stable",
   416  	}
   417  
   418  	// Collect other arguments into a yaml string
   419  	yamlText := fmt.Sprintf(`
   420  name: %s
   421  version: %s
   422  %s`, snapName, version, extraYaml)
   423  
   424  	// Mock the snap on disk
   425  	snapInfo := snaptest.MockSnapInstance(c, instanceName, yamlText, sideInfo)
   426  	if active {
   427  		dir, rev := filepath.Split(snapInfo.MountDir())
   428  		c.Assert(os.Symlink(rev, dir+"current"), check.IsNil)
   429  	}
   430  	c.Assert(snapInfo.InstanceName(), check.Equals, instanceName)
   431  
   432  	c.Assert(os.MkdirAll(snapInfo.DataDir(), 0755), check.IsNil)
   433  	metadir := filepath.Join(snapInfo.MountDir(), "meta")
   434  	guidir := filepath.Join(metadir, "gui")
   435  	c.Assert(os.MkdirAll(guidir, 0755), check.IsNil)
   436  	c.Check(ioutil.WriteFile(filepath.Join(guidir, "icon.svg"), []byte("yadda icon"), 0644), check.IsNil)
   437  
   438  	if d == nil {
   439  		return snapInfo
   440  	}
   441  	st := d.Overlord().State()
   442  	st.Lock()
   443  	defer st.Unlock()
   444  
   445  	var snapst snapstate.SnapState
   446  	snapstate.Get(st, instanceName, &snapst)
   447  	snapst.Active = active
   448  	snapst.Sequence = append(snapst.Sequence, &snapInfo.SideInfo)
   449  	snapst.Current = snapInfo.SideInfo.Revision
   450  	snapst.TrackingChannel = "stable"
   451  	snapst.InstanceKey = instanceKey
   452  
   453  	snapstate.Set(st, instanceName, &snapst)
   454  
   455  	if developer == "" {
   456  		return snapInfo
   457  	}
   458  
   459  	devAcct := assertstest.NewAccount(s.StoreSigning, developer, map[string]interface{}{
   460  		"account-id": developer + "-id",
   461  	}, "")
   462  
   463  	snapInfo.Publisher = snap.StoreAccount{
   464  		ID:          devAcct.AccountID(),
   465  		Username:    devAcct.Username(),
   466  		DisplayName: devAcct.DisplayName(),
   467  		Validation:  devAcct.Validation(),
   468  	}
   469  
   470  	snapDecl, err := s.StoreSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
   471  		"series":       "16",
   472  		"snap-id":      snapID,
   473  		"snap-name":    snapName,
   474  		"publisher-id": devAcct.AccountID(),
   475  		"timestamp":    time.Now().Format(time.RFC3339),
   476  	}, nil, "")
   477  	c.Assert(err, check.IsNil)
   478  
   479  	content, err := ioutil.ReadFile(snapInfo.MountFile())
   480  	c.Assert(err, check.IsNil)
   481  	h := sha3.Sum384(content)
   482  	dgst, err := asserts.EncodeDigest(crypto.SHA3_384, h[:])
   483  	c.Assert(err, check.IsNil)
   484  	snapRev, err := s.StoreSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
   485  		"snap-sha3-384": string(dgst),
   486  		"snap-size":     "999",
   487  		"snap-id":       snapID,
   488  		"snap-revision": fmt.Sprintf("%s", revision),
   489  		"developer-id":  devAcct.AccountID(),
   490  		"timestamp":     time.Now().Format(time.RFC3339),
   491  	}, nil, "")
   492  	c.Assert(err, check.IsNil)
   493  
   494  	assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey(""), devAcct, snapDecl, snapRev)
   495  
   496  	return snapInfo
   497  }
   498  
   499  func handlerCommand(c *check.C, d *daemon.Daemon, req *http.Request) (cmd *daemon.Command, vars map[string]string) {
   500  	m := &mux.RouteMatch{}
   501  	if !d.RouterMatch(req, m) {
   502  		c.Fatalf("no command for URL %q", req.URL)
   503  	}
   504  	cmd, ok := m.Handler.(*daemon.Command)
   505  	if !ok {
   506  		c.Fatalf("no command for URL %q", req.URL)
   507  	}
   508  	return cmd, m.Vars
   509  }
   510  
   511  func (s *apiBaseSuite) checkGetOnly(c *check.C, req *http.Request) {
   512  	if s.d == nil {
   513  		panic("call s.daemon(c) etc in your test first")
   514  	}
   515  
   516  	cmd, _ := handlerCommand(c, s.d, req)
   517  	c.Check(cmd.POST, check.IsNil)
   518  	c.Check(cmd.PUT, check.IsNil)
   519  	c.Check(cmd.GET, check.NotNil)
   520  }
   521  
   522  func (s *apiBaseSuite) req(c *check.C, req *http.Request, u *auth.UserState) daemon.Response {
   523  	if s.d == nil {
   524  		panic("call s.daemon(c) etc in your test first")
   525  	}
   526  
   527  	cmd, vars := handlerCommand(c, s.d, req)
   528  	s.vars = vars
   529  	var f daemon.ResponseFunc
   530  	switch req.Method {
   531  	case "GET":
   532  		f = cmd.GET
   533  	case "POST":
   534  		f = cmd.POST
   535  	case "PUT":
   536  		f = cmd.PUT
   537  	default:
   538  		c.Fatalf("unsupported HTTP method %q", req.Method)
   539  	}
   540  	if f == nil {
   541  		c.Fatalf("no support for %q for %q", req.Method, req.URL)
   542  	}
   543  	return f(cmd, req, u)
   544  }
   545  
   546  func (s *apiBaseSuite) jsonReq(c *check.C, req *http.Request, u *auth.UserState) *daemon.Resp {
   547  	rsp, ok := s.req(c, req, u).(*daemon.Resp)
   548  	c.Assert(ok, check.Equals, true, check.Commentf("expected structured response"))
   549  	return rsp
   550  }
   551  
   552  func (s *apiBaseSuite) syncReq(c *check.C, req *http.Request, u *auth.UserState) *daemon.Resp {
   553  	rsp := s.jsonReq(c, req, u)
   554  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeSync, check.Commentf("expected sync resp: %#v", rsp))
   555  	return rsp
   556  }
   557  
   558  func (s *apiBaseSuite) asyncReq(c *check.C, req *http.Request, u *auth.UserState) *daemon.Resp {
   559  	rsp := s.jsonReq(c, req, u)
   560  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeAsync, check.Commentf("expected async resp: %#v", rsp))
   561  	return rsp
   562  }
   563  
   564  func (s *apiBaseSuite) errorReq(c *check.C, req *http.Request, u *auth.UserState) *daemon.Resp {
   565  	rsp := s.jsonReq(c, req, u)
   566  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError, check.Commentf("expected error resp: %#v", rsp))
   567  	return rsp
   568  }
   569  
   570  func (s *apiBaseSuite) serveHTTP(c *check.C, w http.ResponseWriter, req *http.Request) {
   571  	if s.d == nil {
   572  		panic("call s.daemon(c) etc in your test first")
   573  	}
   574  
   575  	cmd, vars := handlerCommand(c, s.d, req)
   576  	s.vars = vars
   577  
   578  	cmd.ServeHTTP(w, req)
   579  }
   580  
   581  func (s *apiBaseSuite) simulateConflict(name string) {
   582  	if s.d == nil {
   583  		panic("call s.daemon(c) etc in your test first")
   584  	}
   585  
   586  	o := s.d.Overlord()
   587  	st := o.State()
   588  	st.Lock()
   589  	defer st.Unlock()
   590  	t := st.NewTask("link-snap", "...")
   591  	snapsup := &snapstate.SnapSetup{SideInfo: &snap.SideInfo{
   592  		RealName: name,
   593  	}}
   594  	t.Set("snap-setup", snapsup)
   595  	chg := st.NewChange("manip", "...")
   596  	chg.AddTask(t)
   597  }