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