github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/overlord/hookstate/context.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016 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 hookstate
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/json"
    25  	"fmt"
    26  	"sync"
    27  	"sync/atomic"
    28  	"time"
    29  
    30  	"github.com/snapcore/snapd/jsonutil"
    31  	"github.com/snapcore/snapd/overlord/state"
    32  	"github.com/snapcore/snapd/randutil"
    33  	"github.com/snapcore/snapd/snap"
    34  )
    35  
    36  // Context represents the context under which the snap is calling back into snapd.
    37  // It is associated with a task when the callback is happening from within a hook,
    38  // or otherwise considered an ephemeral context in that its associated data will
    39  // be discarded once that individual call is finished.
    40  type Context struct {
    41  	task    *state.Task
    42  	state   *state.State
    43  	setup   *HookSetup
    44  	id      string
    45  	handler Handler
    46  
    47  	cache  map[interface{}]interface{}
    48  	onDone []func() error
    49  
    50  	mutex        sync.Mutex
    51  	mutexChecker int32
    52  }
    53  
    54  // NewContext returns a new context associated with the provided task or
    55  // an ephemeral context if task is nil.
    56  //
    57  // A random ID is generated if contextID is empty.
    58  func NewContext(task *state.Task, state *state.State, setup *HookSetup, handler Handler, contextID string) (*Context, error) {
    59  	if contextID == "" {
    60  		var err error
    61  		contextID, err = randutil.CryptoToken(32)
    62  		if err != nil {
    63  			return nil, err
    64  		}
    65  	}
    66  
    67  	return &Context{
    68  		task:    task,
    69  		state:   state,
    70  		setup:   setup,
    71  		id:      contextID,
    72  		handler: handler,
    73  		cache:   make(map[interface{}]interface{}),
    74  	}, nil
    75  }
    76  
    77  // InstanceName returns the name of the snap instance containing the hook.
    78  func (c *Context) InstanceName() string {
    79  	return c.setup.Snap
    80  }
    81  
    82  // SnapRevision returns the revision of the snap containing the hook.
    83  func (c *Context) SnapRevision() snap.Revision {
    84  	return c.setup.Revision
    85  }
    86  
    87  // Task returns the task associated with the hook or (nil, false) if the context is ephemeral
    88  // and task is not available.
    89  func (c *Context) Task() (*state.Task, bool) {
    90  	return c.task, c.task != nil
    91  }
    92  
    93  // HookName returns the name of the hook in this context.
    94  func (c *Context) HookName() string {
    95  	return c.setup.Hook
    96  }
    97  
    98  // Timeout returns the maximum time this hook can run
    99  func (c *Context) Timeout() time.Duration {
   100  	return c.setup.Timeout
   101  }
   102  
   103  // ID returns the ID of the context.
   104  func (c *Context) ID() string {
   105  	return c.id
   106  }
   107  
   108  // Handler returns the handler for this context
   109  func (c *Context) Handler() Handler {
   110  	return c.handler
   111  }
   112  
   113  // Lock acquires the lock for this context (required for Set/Get, Cache/Cached),
   114  // and OnDone/Done).
   115  func (c *Context) Lock() {
   116  	c.mutex.Lock()
   117  	c.state.Lock()
   118  	atomic.AddInt32(&c.mutexChecker, 1)
   119  }
   120  
   121  // Unlock releases the lock for this context.
   122  func (c *Context) Unlock() {
   123  	atomic.AddInt32(&c.mutexChecker, -1)
   124  	c.state.Unlock()
   125  	c.mutex.Unlock()
   126  }
   127  
   128  func (c *Context) reading() {
   129  	if atomic.LoadInt32(&c.mutexChecker) != 1 {
   130  		panic("internal error: accessing context without lock")
   131  	}
   132  }
   133  
   134  func (c *Context) writing() {
   135  	if atomic.LoadInt32(&c.mutexChecker) != 1 {
   136  		panic("internal error: accessing context without lock")
   137  	}
   138  }
   139  
   140  // Set associates value with key. The provided value must properly marshal and
   141  // unmarshal with encoding/json. Note that the context needs to be locked and
   142  // unlocked by the caller.
   143  func (c *Context) Set(key string, value interface{}) {
   144  	c.writing()
   145  
   146  	var data map[string]*json.RawMessage
   147  	if c.IsEphemeral() {
   148  		data, _ = c.cache["ephemeral-context"].(map[string]*json.RawMessage)
   149  	} else {
   150  		if err := c.task.Get("hook-context", &data); err != nil && err != state.ErrNoState {
   151  			panic(fmt.Sprintf("internal error: cannot unmarshal context: %v", err))
   152  		}
   153  	}
   154  	if data == nil {
   155  		data = make(map[string]*json.RawMessage)
   156  	}
   157  
   158  	marshalledValue, err := json.Marshal(value)
   159  	if err != nil {
   160  		panic(fmt.Sprintf("internal error: cannot marshal context value for %q: %s", key, err))
   161  	}
   162  	raw := json.RawMessage(marshalledValue)
   163  	data[key] = &raw
   164  
   165  	if c.IsEphemeral() {
   166  		c.cache["ephemeral-context"] = data
   167  	} else {
   168  		c.task.Set("hook-context", data)
   169  	}
   170  }
   171  
   172  // Get unmarshals the stored value associated with the provided key into the
   173  // value parameter. Note that the context needs to be locked/unlocked by the
   174  // caller.
   175  func (c *Context) Get(key string, value interface{}) error {
   176  	c.reading()
   177  
   178  	var data map[string]*json.RawMessage
   179  	if c.IsEphemeral() {
   180  		data, _ = c.cache["ephemeral-context"].(map[string]*json.RawMessage)
   181  		if data == nil {
   182  			return state.ErrNoState
   183  		}
   184  	} else {
   185  		if err := c.task.Get("hook-context", &data); err != nil {
   186  			return err
   187  		}
   188  	}
   189  
   190  	raw, ok := data[key]
   191  	if !ok {
   192  		return state.ErrNoState
   193  	}
   194  
   195  	err := jsonutil.DecodeWithNumber(bytes.NewReader(*raw), &value)
   196  	if err != nil {
   197  		return fmt.Errorf("cannot unmarshal context value for %q: %s", key, err)
   198  	}
   199  
   200  	return nil
   201  }
   202  
   203  // State returns the state contained within the context
   204  func (c *Context) State() *state.State {
   205  	return c.state
   206  }
   207  
   208  // Cached returns the cached value associated with the provided key. It returns
   209  // nil if there is no entry for key. Note that the context needs to be locked
   210  // and unlocked by the caller.
   211  func (c *Context) Cached(key interface{}) interface{} {
   212  	c.reading()
   213  
   214  	return c.cache[key]
   215  }
   216  
   217  // Cache associates value with key. The cached value is not persisted. Note that
   218  // the context needs to be locked/unlocked by the caller.
   219  func (c *Context) Cache(key, value interface{}) {
   220  	c.writing()
   221  
   222  	c.cache[key] = value
   223  }
   224  
   225  // OnDone requests the provided function to be run once the context knows it's
   226  // complete. This can be called multiple times; each function will be called in
   227  // the order in which they were added. Note that the context needs to be locked
   228  // and unlocked by the caller.
   229  func (c *Context) OnDone(f func() error) {
   230  	c.writing()
   231  
   232  	c.onDone = append(c.onDone, f)
   233  }
   234  
   235  // Done is called to notify the context that its hook has exited successfully.
   236  // It will call all of the functions added in OnDone (even if one of them
   237  // returns an error) and will return the first error encountered. Note that the
   238  // context needs to be locked/unlocked by the caller.
   239  func (c *Context) Done() error {
   240  	c.reading()
   241  
   242  	var firstErr error
   243  	for _, f := range c.onDone {
   244  		if err := f(); err != nil && firstErr == nil {
   245  			firstErr = err
   246  		}
   247  	}
   248  
   249  	return firstErr
   250  }
   251  
   252  func (c *Context) IsEphemeral() bool {
   253  	return c.task == nil
   254  }