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 }