graphvent/thread.go

653 lines
19 KiB
Go

package graphvent
import (
"fmt"
"time"
"sync"
"errors"
"encoding/json"
)
// Assumed that thread is already locked for signal
2023-07-24 16:04:56 -06:00
func (thread *Thread) Process(context *StateContext, signal GraphSignal) error {
2023-07-24 17:07:27 -06:00
context.Graph.Log.Logf("signal", "THREAD_PROCESS: %s", thread.ID())
2023-07-24 17:07:27 -06:00
var err error
switch signal.Direction() {
case Up:
err = UseStates(context, thread, NewLockInfo(thread, []string{"parent"}), func(context *StateContext) error {
2023-07-24 16:04:56 -06:00
if thread.Parent != nil {
return Signal(context, thread.Parent, thread, signal)
} else {
return nil
}
})
case Down:
err = UseStates(context, thread, NewLockInfo(thread, []string{"children"}), func(context *StateContext) error {
2023-07-24 16:04:56 -06:00
for _, info := range(thread.Children) {
err := Signal(context, info.Child, thread, signal)
if err != nil {
return err
}
2023-06-23 21:21:14 -06:00
}
return nil
})
case Direct:
err = nil
default:
return fmt.Errorf("Invalid signal direction %d", signal.Direction())
}
if err != nil {
return err
}
2023-07-24 16:04:56 -06:00
thread.Chan <- signal
2023-07-24 17:07:27 -06:00
return thread.Lockable.Process(context, signal)
}
2023-07-09 15:59:41 -06:00
// Requires thread and childs thread to be locked for write
2023-07-24 16:04:56 -06:00
func UnlinkThreads(ctx * Context, node ThreadNode, child_node ThreadNode) error {
thread := node.ThreadHandle()
child := child_node.ThreadHandle()
_, is_child := thread.Children[child_node.ID()]
if is_child == false {
return fmt.Errorf("UNLINK_THREADS_ERR: %s is not a child of %s", child.ID(), thread.ID())
}
2023-07-24 16:04:56 -06:00
child.Parent = nil
delete(thread.Children, child.ID())
return nil
}
2023-07-24 16:04:56 -06:00
func checkIfChild(context *StateContext, target ThreadNode, cur ThreadNode) bool {
for _, info := range(cur.ThreadHandle().Children) {
if info.Child.ID() == target.ID() {
return true
}
is_child := false
UpdateStates(context, cur, NewLockMap(
2023-07-24 16:04:56 -06:00
NewLockInfo(info.Child, []string{"children"}),
), func(context *StateContext) error {
2023-07-24 16:04:56 -06:00
is_child = checkIfChild(context, target, info.Child)
return nil
})
if is_child {
return true
}
}
return false
}
// Links child to parent with info as the associated info
// Continues the write context with princ, getting children for thread and parent for child
2023-07-24 16:04:56 -06:00
func LinkThreads(context *StateContext, princ Node, thread_node ThreadNode, info ChildInfo) error {
if context == nil || thread_node == nil || info.Child == nil {
return fmt.Errorf("invalid input")
}
2023-07-24 16:04:56 -06:00
thread := thread_node.ThreadHandle()
child := info.Child.ThreadHandle()
if thread.ID() == child.ID() {
return fmt.Errorf("Will not link %s as a child of itself", thread.ID())
}
return UpdateStates(context, princ, LockMap{
child.ID(): LockInfo{Node: child, Resources: []string{"parent"}},
thread.ID(): LockInfo{Node: thread, Resources: []string{"children"}},
}, func(context *StateContext) error {
2023-07-24 16:04:56 -06:00
if child.Parent != nil {
return fmt.Errorf("EVENT_LINK_ERR: %s already has a parent, cannot link as child", child.ID())
}
if checkIfChild(context, thread, child) == true {
return fmt.Errorf("EVENT_LINK_ERR: %s is a child of %s so cannot add as parent", thread.ID(), child.ID())
}
if checkIfChild(context, child, thread) == true {
return fmt.Errorf("EVENT_LINK_ERR: %s is already a parent of %s so will not add again", thread.ID(), child.ID())
}
2023-07-24 16:04:56 -06:00
// TODO check for info types
2023-07-24 16:04:56 -06:00
thread.Children[child.ID()] = info
child.Parent = thread_node
return nil
})
}
2023-07-24 16:04:56 -06:00
type ThreadAction func(*Context, ThreadNode)(string, error)
2023-07-09 15:59:41 -06:00
type ThreadActions map[string]ThreadAction
2023-07-24 16:04:56 -06:00
type ThreadHandler func(*Context, ThreadNode, GraphSignal)(string, error)
2023-07-09 15:59:41 -06:00
type ThreadHandlers map[string]ThreadHandler
2023-07-24 16:04:56 -06:00
type InfoType string
func (t InfoType) String() string {
return string(t)
}
// Data required by a parent thread to restore it's children
type ParentThreadInfo struct {
Start bool `json:"start"`
StartAction string `json:"start_action"`
RestoreAction string `json:"restore_action"`
}
func NewParentThreadInfo(start bool, start_action string, restore_action string) ParentThreadInfo {
return ParentThreadInfo{
Start: start,
StartAction: start_action,
RestoreAction: restore_action,
}
}
2023-07-24 16:04:56 -06:00
type ChildInfo struct {
Child ThreadNode
Infos map[InfoType]interface{}
}
2023-07-09 15:59:41 -06:00
2023-07-24 16:04:56 -06:00
func NewChildInfo(child ThreadNode, infos map[InfoType]interface{}) ChildInfo {
if infos == nil {
infos = map[InfoType]interface{}{}
}
2023-07-09 15:59:41 -06:00
2023-07-24 16:04:56 -06:00
return ChildInfo{
Child: child,
Infos: infos,
}
}
type QueuedAction struct {
Timeout time.Time
Action string
}
type ThreadNode interface {
LockableNode
ThreadHandle() *Thread
}
type Thread struct {
Lockable
Actions ThreadActions
Handlers ThreadHandlers
TimeoutChan <-chan time.Time
Chan chan GraphSignal
ChildWaits sync.WaitGroup
Active bool
ActiveLock sync.Mutex
StateName string
Parent ThreadNode
Children map[NodeID]ChildInfo
InfoTypes []InfoType
TimeoutAction string
Timeout time.Time
2023-07-09 15:59:41 -06:00
}
2023-07-24 16:04:56 -06:00
func (thread *Thread) ThreadHandle() *Thread {
return thread
}
func (thread *Thread) Type() NodeType {
2023-07-24 17:07:27 -06:00
return NodeType("thread")
2023-07-09 16:03:42 -06:00
}
2023-07-24 16:04:56 -06:00
func (thread *Thread) Serialize() ([]byte, error) {
thread_json := NewThreadJSON(thread)
2023-07-09 15:59:41 -06:00
return json.MarshalIndent(&thread_json, "", " ")
}
2023-07-24 16:04:56 -06:00
func (thread *Thread) ChildList() []ThreadNode {
ret := make([]ThreadNode, len(thread.Children))
i := 0
for _, info := range(thread.Children) {
ret[i] = info.Child
i += 1
}
return ret
2023-07-09 15:59:41 -06:00
}
2023-07-24 16:04:56 -06:00
type ThreadJSON struct {
2023-07-21 00:06:11 -06:00
Parent string `json:"parent"`
2023-07-24 16:04:56 -06:00
Children map[string]map[string]interface{} `json:"children"`
2023-07-09 15:59:41 -06:00
Timeout time.Time `json:"timeout"`
TimeoutAction string `json:"timeout_action"`
StateName string `json:"state_name"`
2023-07-24 16:04:56 -06:00
LockableJSON
2023-07-09 15:59:41 -06:00
}
2023-07-24 16:04:56 -06:00
func NewThreadJSON(thread *Thread) ThreadJSON {
children := map[string]map[string]interface{}{}
for id, info := range(thread.Children) {
tmp := map[string]interface{}{}
for name, i := range(info.Infos) {
tmp[name.String()] = i
}
children[id.String()] = tmp
2023-07-09 15:59:41 -06:00
}
2023-07-21 00:06:11 -06:00
parent_id := ""
2023-07-24 16:04:56 -06:00
if thread.Parent != nil {
parent_id = thread.Parent.ID().String()
2023-07-09 15:59:41 -06:00
}
2023-07-24 16:04:56 -06:00
lockable_json := NewLockableJSON(&thread.Lockable)
2023-07-09 15:59:41 -06:00
2023-07-24 16:04:56 -06:00
return ThreadJSON{
2023-07-09 15:59:41 -06:00
Parent: parent_id,
Children: children,
2023-07-24 16:04:56 -06:00
Timeout: thread.Timeout,
TimeoutAction: thread.TimeoutAction,
StateName: thread.StateName,
LockableJSON: lockable_json,
2023-07-09 15:59:41 -06:00
}
}
2023-07-24 16:04:56 -06:00
func LoadThread(ctx *Context, id NodeID, data []byte, nodes NodeMap) (Node, error) {
var j ThreadJSON
2023-07-09 15:59:41 -06:00
err := json.Unmarshal(data, &j)
if err != nil {
return nil, err
}
2023-07-24 16:04:56 -06:00
thread := NewThread(id, j.Name, j.StateName, nil, BaseThreadActions, BaseThreadHandlers)
2023-07-09 15:59:41 -06:00
nodes[id] = &thread
2023-07-24 16:04:56 -06:00
err = RestoreThread(ctx, &thread, j, nodes)
2023-07-09 15:59:41 -06:00
if err != nil {
return nil, err
}
return &thread, nil
}
2023-07-24 16:04:56 -06:00
func RestoreThread(ctx *Context, thread *Thread, j ThreadJSON, nodes NodeMap) error {
if j.TimeoutAction != "" {
2023-07-24 16:04:56 -06:00
thread.Timeout = j.Timeout
thread.TimeoutAction = j.TimeoutAction
}
2023-07-09 15:59:41 -06:00
2023-07-21 00:06:11 -06:00
if j.Parent != "" {
parent_id, err := ParseID(j.Parent)
if err != nil {
return err
}
p, err := LoadNodeRecurse(ctx, parent_id, nodes)
2023-07-09 15:59:41 -06:00
if err != nil {
return err
}
2023-07-24 16:04:56 -06:00
p_t, ok := p.(ThreadNode)
2023-07-09 15:59:41 -06:00
if ok == false {
return err
}
2023-07-24 16:04:56 -06:00
thread.Parent = p_t
2023-07-09 15:59:41 -06:00
}
for id_str, info_raw := range(j.Children) {
id, err := ParseID(id_str)
if err != nil {
return err
}
2023-07-24 16:04:56 -06:00
2023-07-09 15:59:41 -06:00
child_node, err := LoadNodeRecurse(ctx, id, nodes)
if err != nil {
return err
}
2023-07-24 16:04:56 -06:00
child_t, ok := child_node.(ThreadNode)
2023-07-09 15:59:41 -06:00
if ok == false {
return fmt.Errorf("%+v is not a Thread as expected", child_node)
}
2023-07-24 16:04:56 -06:00
parsed_info, err := DeserializeChildInfo(ctx, info_raw)
2023-07-11 16:39:47 -06:00
if err != nil {
return err
2023-07-09 15:59:41 -06:00
}
2023-07-24 16:04:56 -06:00
thread.Children[id] = ChildInfo{child_t, parsed_info}
}
return RestoreLockable(ctx, &thread.Lockable, j.LockableJSON, nodes)
}
var deserializers = map[InfoType]func(interface{})(interface{}, error) {
2023-07-24 17:07:27 -06:00
"parent": func(raw interface{})(interface{}, error) {
m, ok := raw.(map[string]interface{})
if ok == false {
return nil, fmt.Errorf("Failed to cast parent info to map")
}
start, ok := m["start"].(bool)
if ok == false {
return nil, fmt.Errorf("Failed to get start from parent info")
}
start_action, ok := m["start_action"].(string)
if ok == false {
return nil, fmt.Errorf("Failed to get start_action from parent info")
}
restore_action, ok := m["restore_action"].(string)
if ok == false {
return nil, fmt.Errorf("Failed to get restore_action from parent info")
}
2023-07-24 16:04:56 -06:00
2023-07-24 17:07:27 -06:00
return &ParentThreadInfo{
Start: start,
StartAction: start_action,
RestoreAction: restore_action,
}, nil
},
2023-07-24 16:04:56 -06:00
}
func DeserializeChildInfo(ctx *Context, infos_raw map[string]interface{}) (map[InfoType]interface{}, error) {
ret := map[InfoType]interface{}{}
for type_str, info_raw := range(infos_raw) {
info_type := InfoType(type_str)
deserializer, exists := deserializers[info_type]
if exists == false {
return nil, fmt.Errorf("No deserializer for %s", info_type)
}
var err error
ret[info_type], err = deserializer(info_raw)
if err != nil {
return nil, err
}
2023-07-09 15:59:41 -06:00
}
2023-07-24 16:04:56 -06:00
return ret, nil
2023-07-09 15:59:41 -06:00
}
const THREAD_SIGNAL_BUFFER_SIZE = 128
2023-07-24 16:04:56 -06:00
func NewThread(id NodeID, name string, state_name string, info_types []InfoType, actions ThreadActions, handlers ThreadHandlers) Thread {
return Thread{
Lockable: NewLockable(id, name),
InfoTypes: info_types,
StateName: state_name,
Chan: make(chan GraphSignal, THREAD_SIGNAL_BUFFER_SIZE),
Children: map[NodeID]ChildInfo{},
Actions: actions,
Handlers: handlers,
}
}
func (thread *Thread) SetActive(active bool) error {
thread.ActiveLock.Lock()
defer thread.ActiveLock.Unlock()
if thread.Active == true && active == true {
return fmt.Errorf("%s is active, cannot set active", thread.ID())
} else if thread.Active == false && active == false {
return fmt.Errorf("%s is already inactive, canot set inactive", thread.ID())
2023-07-09 15:59:41 -06:00
}
2023-07-24 16:04:56 -06:00
thread.Active = active
return nil
}
func (thread *Thread) SetState(state string) error {
thread.StateName = state
return nil
}
// Requires the read permission of threads children
2023-07-24 16:04:56 -06:00
func FindChild(context *StateContext, princ Node, node ThreadNode, id NodeID) ThreadNode {
if node == nil {
panic("cannot recurse through nil")
}
2023-07-24 16:04:56 -06:00
thread := node.ThreadHandle()
if id == thread.ID() {
return thread
}
2023-07-24 16:04:56 -06:00
for _, info := range thread.Children {
var result ThreadNode
UseStates(context, princ, NewLockInfo(info.Child, []string{"children"}), func(context *StateContext) error {
result = FindChild(context, princ, info.Child, id)
return nil
})
if result != nil {
return result
}
}
return nil
}
2023-07-24 16:04:56 -06:00
func ChildGo(ctx * Context, thread *Thread, child ThreadNode, first_action string) {
thread.ChildWaits.Add(1)
go func(child ThreadNode) {
ctx.Log.Logf("thread", "THREAD_START_CHILD: %s from %s", thread.ID(), child.ID())
2023-07-24 16:04:56 -06:00
defer thread.ChildWaits.Done()
err := ThreadLoop(ctx, child, first_action)
if err != nil {
2023-07-24 17:07:27 -06:00
ctx.Log.Logf("thread", "THREAD_CHILD_RUN_ERR: %s %s", child.ID(), err)
} else {
ctx.Log.Logf("thread", "THREAD_CHILD_RUN_DONE: %s", child.ID())
}
}(child)
}
// Main Loop for Threads, starts a write context, so cannot be called from a write or read context
2023-07-24 16:04:56 -06:00
func ThreadLoop(ctx * Context, node ThreadNode, first_action string) error {
// Start the thread, error if double-started
2023-07-24 16:04:56 -06:00
thread := node.ThreadHandle()
ctx.Log.Logf("thread", "THREAD_LOOP_START: %s - %s", thread.ID(), first_action)
2023-07-09 15:59:41 -06:00
err := thread.SetActive(true)
if err != nil {
ctx.Log.Logf("thread", "THREAD_LOOP_START_ERR: %e", err)
return err
}
next_action := first_action
for next_action != "" {
2023-07-24 16:04:56 -06:00
action, exists := thread.Actions[next_action]
if exists == false {
error_str := fmt.Sprintf("%s is not a valid action", next_action)
return errors.New(error_str)
}
ctx.Log.Logf("thread", "THREAD_ACTION: %s - %s", thread.ID(), next_action)
2023-07-24 16:04:56 -06:00
next_action, err = action(ctx, node)
if err != nil {
return err
}
}
2023-07-09 15:59:41 -06:00
err = thread.SetActive(false)
if err != nil {
ctx.Log.Logf("thread", "THREAD_LOOP_STOP_ERR: %e", err)
return err
}
2023-06-26 23:15:40 -06:00
ctx.Log.Logf("thread", "THREAD_LOOP_DONE: %s", thread.ID())
return nil
}
2023-07-24 16:04:56 -06:00
func ThreadChildLinked(ctx *Context, node ThreadNode, signal GraphSignal) (string, error) {
thread := node.ThreadHandle()
ctx.Log.Logf("thread", "THREAD_CHILD_LINKED: %+v", signal)
context := NewWriteContext(ctx)
err := UpdateStates(context, thread, NewLockMap(
NewLockInfo(thread, []string{"children"}),
), func(context *StateContext) error {
sig, ok := signal.(IDSignal)
if ok == false {
ctx.Log.Logf("thread", "THREAD_NODE_LINKED_BAD_CAST")
return nil
}
2023-07-24 16:04:56 -06:00
info, exists := thread.Children[sig.ID]
if exists == false {
ctx.Log.Logf("thread", "THREAD_NODE_LINKED: %s is not a child of %s", sig.ID)
return nil
}
2023-07-24 16:04:56 -06:00
parent_info, exists := info.Infos["parent"].(*ParentThreadInfo)
if exists == false {
panic("ran ThreadChildLinked from a thread that doesn't require 'parent' child info. library party foul")
}
2023-07-24 16:04:56 -06:00
if parent_info.Start == true {
ChildGo(ctx, thread, info.Child, parent_info.StartAction)
}
return nil
})
if err != nil {
} else {
}
return "wait", nil
}
2023-07-24 16:04:56 -06:00
// Helper function to start a child from a thread during a signal handler
// Starts a write context, so cannot be called from either a write or read context
func ThreadStartChild(ctx *Context, node ThreadNode, signal GraphSignal) (string, error) {
sig, ok := signal.(StartChildSignal)
if ok == false {
return "wait", nil
}
2023-07-24 16:04:56 -06:00
thread := node.ThreadHandle()
context := NewWriteContext(ctx)
2023-07-24 16:04:56 -06:00
return "wait", UpdateStates(context, thread, NewLockInfo(thread, []string{"children"}), func(context *StateContext) error {
info, exists:= thread.Children[sig.ID]
if exists == false {
return fmt.Errorf("%s is not a child of %s", sig.ID, thread.ID())
2023-07-21 14:05:39 -06:00
}
2023-07-24 16:04:56 -06:00
return UpdateStates(context, thread, NewLockInfo(info.Child, []string{"start"}), func(context *StateContext) error {
2023-07-21 14:05:39 -06:00
2023-07-24 16:04:56 -06:00
parent_info, exists := info.Infos["parent"].(*ParentThreadInfo)
if exists == false {
return fmt.Errorf("Called ThreadStartChild from a thread that doesn't require parent child info")
}
parent_info.Start = true
ChildGo(ctx, thread, info.Child, sig.Action)
2023-07-21 14:05:39 -06:00
return nil
})
2023-07-21 14:05:39 -06:00
})
}
// Helper function to restore threads that should be running from a parents restore action
// Starts a write context, so cannot be called from either a write or read context
2023-07-24 16:04:56 -06:00
func ThreadRestore(ctx * Context, node ThreadNode, start bool) error {
thread := node.ThreadHandle()
context := NewWriteContext(ctx)
return UpdateStates(context, thread, NewLockInfo(thread, []string{"children"}), func(context *StateContext) error {
2023-07-24 16:04:56 -06:00
return UpdateStates(context, thread, LockList(thread.ChildList(), []string{"start"}), func(context *StateContext) error {
for _, info := range(thread.Children) {
parent_info := info.Infos["parent"].(*ParentThreadInfo)
if parent_info.Start == true && info.Child.ThreadHandle().StateName != "finished" {
ctx.Log.Logf("thread", "THREAD_RESTORED: %s -> %s", thread.ID(), info.Child.ID())
if start == true {
2023-07-24 16:04:56 -06:00
ChildGo(ctx, thread, info.Child, parent_info.StartAction)
} else {
2023-07-24 16:04:56 -06:00
ChildGo(ctx, thread, info.Child, parent_info.RestoreAction)
}
}
}
return nil
})
})
}
// Helper function to be called during a threads start action, sets the thread state to started
// Starts a write context, so cannot be called from either a write or read context
2023-07-24 16:04:56 -06:00
// Returns "wait", nil on success, so the first return value can be ignored safely
func ThreadStart(ctx * Context, node ThreadNode) (string, error) {
thread := node.ThreadHandle()
context := NewWriteContext(ctx)
2023-07-24 16:04:56 -06:00
return "wait", UpdateStates(context, thread, NewLockInfo(thread, []string{"state"}), func(context *StateContext) error {
err := LockLockables(context, map[NodeID]LockableNode{thread.ID(): thread}, thread)
if err != nil {
return err
}
2023-07-09 15:59:41 -06:00
return thread.SetState("started")
})
}
2023-07-24 16:04:56 -06:00
func ThreadWait(ctx * Context, node ThreadNode) (string, error) {
thread := node.ThreadHandle()
ctx.Log.Logf("thread", "THREAD_WAIT: %s TIMEOUT: %+v", thread.ID(), thread.Timeout)
2023-06-24 19:48:59 -06:00
for {
select {
2023-07-24 16:04:56 -06:00
case signal := <- thread.Chan:
ctx.Log.Logf("thread", "THREAD_SIGNAL: %s %+v", thread.ID(), signal)
2023-07-24 16:04:56 -06:00
signal_fn, exists := thread.Handlers[signal.Type()]
2023-06-24 19:48:59 -06:00
if exists == true {
ctx.Log.Logf("thread", "THREAD_HANDLER: %s - %s", thread.ID(), signal.Type())
return signal_fn(ctx, thread, signal)
} else {
ctx.Log.Logf("thread", "THREAD_NOHANDLER: %s - %s", thread.ID(), signal.Type())
2023-06-24 19:48:59 -06:00
}
2023-07-24 16:04:56 -06:00
case <- thread.TimeoutChan:
2023-07-02 12:14:04 -06:00
timeout_action := ""
context := NewWriteContext(ctx)
err := UpdateStates(context, thread, NewLockMap(NewLockInfo(thread, []string{"timeout"})), func(context *StateContext) error {
2023-07-24 16:04:56 -06:00
timeout_action = thread.TimeoutAction
thread.TimeoutChan = nil
thread.TimeoutAction = ""
thread.Timeout = time.Time{}
2023-07-02 12:14:04 -06:00
return nil
})
if err != nil {
ctx.Log.Logf("thread", "THREAD_TIMEOUT_ERR: %s - %e", thread.ID(), err)
}
ctx.Log.Logf("thread", "THREAD_TIMEOUT %s - NEXT_STATE: %s", thread.ID(), timeout_action)
return timeout_action, nil
2023-06-24 19:48:59 -06:00
}
}
}
2023-07-24 16:04:56 -06:00
func ThreadFinish(ctx *Context, node ThreadNode) (string, error) {
thread := node.ThreadHandle()
context := NewWriteContext(ctx)
2023-07-24 16:04:56 -06:00
return "", UpdateStates(context, thread, NewLockInfo(thread, []string{"state"}), func(context *StateContext) error {
err := thread.SetState("finished")
if err != nil {
return err
}
2023-07-24 16:04:56 -06:00
return UnlockLockables(context, map[NodeID]LockableNode{thread.ID(): thread}, thread)
})
}
var ThreadAbortedError = errors.New("Thread aborted by signal")
// Default thread action function for "abort", sends a signal and returns a ThreadAbortedError
2023-07-24 16:04:56 -06:00
func ThreadAbort(ctx * Context, node ThreadNode, signal GraphSignal) (string, error) {
thread := node.ThreadHandle()
context := NewReadContext(ctx)
2023-07-24 01:41:47 -06:00
err := Signal(context, thread, thread, NewStatusSignal("aborted", thread.ID()))
if err != nil {
return "", err
}
return "", ThreadAbortedError
2023-06-23 21:21:14 -06:00
}
// Default thread action for "stop", sends a signal and returns no error
2023-07-24 16:04:56 -06:00
func ThreadStop(ctx * Context, node ThreadNode, signal GraphSignal) (string, error) {
thread := node.ThreadHandle()
context := NewReadContext(ctx)
2023-07-24 01:41:47 -06:00
err := Signal(context, thread, thread, NewStatusSignal("stopped", thread.ID()))
return "finish", err
}
// Default thread actions
2023-07-02 12:14:04 -06:00
var BaseThreadActions = ThreadActions{
"wait": ThreadWait,
2023-07-24 16:04:56 -06:00
"start": ThreadStart,
"finish": ThreadFinish,
2023-07-02 12:14:04 -06:00
}
// Default thread signal handlers
2023-07-02 12:14:04 -06:00
var BaseThreadHandlers = ThreadHandlers{
"abort": ThreadAbort,
"stop": ThreadStop,
2023-07-02 12:14:04 -06:00
}