| // Copyright (C) 2016 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package fsm |
| |
| import ( |
| "errors" |
| "sync" |
| |
| "fmt" |
| |
| "android.googlesource.com/platform/tools/gpu/framework/log" |
| "android.googlesource.com/platform/tools/gpu/framework/task" |
| ) |
| |
| type pending struct { |
| event Event |
| state *runningState |
| hooks []HookFunc |
| done task.Task |
| } |
| |
| type active struct { |
| state *runningState |
| ctx log.Context // ctx for the running state |
| cancel task.CancelFunc // cancel for the running state |
| } |
| |
| // Instance is a running copy of a compiled fsm. |
| // All public methods on this class are concurrent safe. |
| type Instance struct { |
| mutex sync.Mutex |
| data interface{} |
| next pending |
| running active |
| controller task.Handle |
| } |
| |
| type fsmKeyType string |
| |
| var ( |
| fsmKey fsmKeyType = "FSM" |
| fsmExit = errors.New("fsm exit") |
| ) |
| |
| // Get returns the running fsm as stored in the context, if there is one. |
| func Get(ctx log.Context) *Instance { |
| return ctx.Value(fsmKey).(*Instance) |
| } |
| |
| // RunWith executes an FSM by creating a new instance, and running it to completion. |
| // It passes the supplied data argument to the instance and all it's builders. |
| // It returns the running error if one occurred. |
| func RunWith(ctx log.Context, data interface{}, c *Compiled) error { |
| fsm := &Instance{data: data} |
| ctx = ctx.HiddenValue(fsmKey, fsm) |
| // first build the machine structure |
| if err := fsm.build(ctx, c); err != nil { |
| return err |
| } |
| // and prepare the running information |
| defer func() { |
| fsm.next = pending{} |
| fsm.running = active{} |
| }() |
| fsm.running = active{ctx: ctx} |
| // start the controller if present |
| fsm.controller = task.Go(ctx, func(log.Context) error { |
| if c.Controller != nil { |
| if err := c.Controller(ctx); err != nil { |
| fsm.Trigger(ctx, controllerError) |
| return err |
| } |
| } |
| return nil |
| }) |
| if err := fsm.run(ctx); err == fsmExit { |
| return nil |
| } else { |
| return err |
| } |
| } |
| |
| // Run executes an FSM by creating a new instance, and running it to completion. |
| // It returns the running error if one occurred. |
| func Run(ctx log.Context, c *Compiled) error { |
| return RunWith(ctx, nil, c) |
| } |
| |
| // Trigger delivers an event to the fsm, possibly causing it to change state. |
| func (fsm *Instance) Trigger(ctx log.Context, e Event) (task.Signal, error) { |
| fsm.mutex.Lock() |
| defer fsm.mutex.Unlock() |
| if e == nil { |
| e = Next |
| } |
| err := fsm.transition(ctx, e) |
| if err != nil { |
| return nil, err |
| } |
| signal, transitioned := task.NewSignal() |
| fsm.next.done = transitioned |
| return signal, nil |
| } |
| |
| // State returns the current active state of the fsm. |
| func (fsm *Instance) State() StateID { |
| fsm.mutex.Lock() |
| defer fsm.mutex.Unlock() |
| return fsm.running.state.name |
| } |
| |
| // Return the data the fsm was run with. |
| func (fsm *Instance) Data() interface{} { |
| return fsm.data |
| } |
| |
| func emptyStateFunc(log.Context) error { return nil } |
| func emptyStateBuilder(log.Context, interface{}) (task.Task, error) { |
| return emptyStateFunc, nil |
| } |
| |
| // build the running structures from the compiled ones |
| func (fsm *Instance) build(ctx log.Context, c *Compiled) error { |
| stateList := make([]runningState, len(c.StateList)) |
| stateMap := make(map[StateID]*runningState, len(c.StateList)) |
| for i, name := range c.StateList { |
| do := c.States[name].Do |
| if do == nil { |
| do = emptyStateBuilder |
| } |
| stateFunc, err := do(ctx, fsm.data) |
| if err != nil { |
| return err |
| } |
| if stateFunc == nil { |
| stateFunc = emptyStateFunc |
| } |
| stateList[i] = runningState{ |
| fsm: fsm, |
| name: name, |
| do: stateFunc, |
| transitions: map[Event]runningTransition{}, |
| } |
| stateMap[name] = &stateList[i] |
| } |
| for i, name := range c.StateList { |
| rs := &stateList[i] |
| for _, t := range c.States[name].Transitions { |
| rt := runningTransition{to: stateMap[t.To.Name]} |
| rt.hooks = make([]HookFunc, len(t.Hooks)) |
| for i, h := range t.Hooks { |
| hook, err := h(ctx, fsm.data) |
| if err != nil { |
| return err |
| } |
| rt.hooks[i] = hook |
| } |
| rs.transitions[t.Event] = rt |
| } |
| } |
| fsm.next = pending{ |
| event: "initial", |
| state: stateMap[c.Initial.Name], |
| } |
| return nil |
| } |
| |
| // run the instance |
| // the instance mutex must *not* be held when this function is invoked. |
| func (fsm *Instance) run(ctx log.Context) error { |
| var from StateID |
| // loop until the cancel context happens |
| for !task.Stopped(ctx) { |
| active, event, hooks, err := fsm.nextState(ctx) |
| if err != nil { |
| return err |
| } |
| // run transition hooks |
| for _, hook := range hooks { |
| hook(ctx, fsm, from, event) |
| } |
| // run the current state |
| if err := active.state.do(active.ctx); err != nil { |
| return err |
| } |
| from = active.state.name |
| } |
| return task.StopReason(ctx) |
| } |
| |
| // perform the switch to a new state. |
| // the instance mutex must *not* be held when this function is invoked. |
| func (fsm *Instance) nextState(ctx log.Context) (active, Event, []HookFunc, error) { |
| fsm.mutex.Lock() |
| defer fsm.mutex.Unlock() |
| next := fsm.next |
| fsm.next = pending{} |
| if next.state == nil { |
| // A state just quit with no queued transition, try the default one |
| fsm.running.cancel = nil |
| err := fsm.transition(fsm.running.ctx, Next) |
| next = fsm.next |
| fsm.next = pending{} |
| if err != nil { |
| return active{}, nil, nil, err |
| } |
| } |
| fsm.running.state = next.state |
| fsm.running.ctx, fsm.running.cancel = task.WithCancel(ctx.Enter(fmt.Sprint(fsm.running.state.name))) |
| if next.done != nil { |
| next.done(ctx) |
| next.done = nil |
| } |
| return fsm.running, next.event, next.hooks, nil |
| } |
| |
| // transition is the internal state transition function. |
| // the machine mutex *must* be held when this function is invoked. |
| func (fsm *Instance) transition(ctx log.Context, e Event) error { |
| if fsm.running.state == nil { |
| return ctx.V("Event", e).AsError("Transition when not running") |
| } |
| transition, ok := fsm.running.state.transitions[e] |
| if !ok { |
| if e == controllerError { |
| transition = runningTransition{to: &runningState{do: func(log.Context) error { |
| return fsm.controller.Result() |
| }}} |
| } else { |
| // event matches no valid transition |
| return ctx.V("Event", e).AsError("Invalid event") |
| } |
| } |
| // Cancel the running task, if we have not already |
| if fsm.running.cancel != nil { |
| // TODO: hand cancel events to a hook? |
| fsm.running.cancel() |
| // clear the cancel so we don't invoke it again if another transition interrupts |
| fsm.running.cancel = nil |
| } |
| // If we have a pending transition, mark it complete before building the new signal |
| if fsm.next.done != nil { |
| fsm.next.done(ctx) |
| } |
| fsm.next = pending{ |
| event: e, |
| state: transition.to, |
| hooks: transition.hooks, |
| } |
| return nil |
| } |