From c95ff42dee36dcd2979e9e242a9e66d58f7cd0ef Mon Sep 17 00:00:00 2001 From: TorchedSammy <38820196+TorchedSammy@users.noreply.github.com> Date: Tue, 12 Apr 2022 19:28:25 -0400 Subject: [PATCH] feat: add timer pool and api (closes #135) adds a map (but lets call it a pool) of all running timers. this makes us able to keep track of all running intervals and timeouts. it also means hilbish can wait for them to be done before exiting (it only waits when non interactive). this introduces the `hilbish.timers` interface, documented by `doc timers`. the `hilbish.interval` and `hilbish.timeout` functions return a timer object now. --- api.go | 44 +++++++------------- docs/timers.txt | 30 ++++++++++++++ main.go | 14 ++++++- timer.go | 106 ++++++++++++++++++++++++++++++++++++++++++++++++ timerhandler.go | 102 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 264 insertions(+), 32 deletions(-) create mode 100644 docs/timers.txt create mode 100644 timer.go create mode 100644 timerhandler.go diff --git a/api.go b/api.go index 5f3a244..5c139f3 100644 --- a/api.go +++ b/api.go @@ -131,6 +131,11 @@ Check out the {blue}{bold}guide{reset} command to get started. jobModule := jobs.loader(rtm) util.Document(jobModule, "(Background) job interface.") mod.Set(rt.StringValue("jobs"), rt.TableValue(jobModule)) + + timers = newTimerHandler() + timerModule := timers.loader(rtm) + util.Document(timerModule, "Timer interface, for control of all intervals and timeouts.") + mod.Set(rt.StringValue("timers"), rt.TableValue(timerModule)) return rt.TableValue(mod), nil } @@ -481,15 +486,11 @@ func hltimeout(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } - timeout := time.Duration(ms) * time.Millisecond - time.Sleep(timeout) - - _, err = rt.Call1(l.MainThread(), rt.FunctionValue(cb)) - if err != nil { - return nil, err - } - - return c.Next(), nil + interval := time.Duration(ms) * time.Millisecond + timer := timers.create(timerTimeout, interval, cb) + timer.start() + + return c.PushingNext1(t.Runtime, timer.lua()), nil } // interval(cb, time) @@ -508,29 +509,12 @@ func hlinterval(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err != nil { return nil, err } + interval := time.Duration(ms) * time.Millisecond + timer := timers.create(timerInterval, interval, cb) + timer.start() - ticker := time.NewTicker(interval) - stop := make(chan rt.Value) - - go func() { - for { - select { - case <-ticker.C: - _, err := rt.Call1(l.MainThread(), rt.FunctionValue(cb)) - if err != nil { - fmt.Fprintln(os.Stderr, "Error in interval function:\n\n", err) - stop <- rt.BoolValue(true) // stop the interval - } - case <-stop: - ticker.Stop() - return - } - } - }() - - // TODO: return channel - return c.Next(), nil + return c.PushingNext1(t.Runtime, timer.lua()), nil } // complete(scope, cb) diff --git a/docs/timers.txt b/docs/timers.txt new file mode 100644 index 0000000..c5a456b --- /dev/null +++ b/docs/timers.txt @@ -0,0 +1,30 @@ +If you ever want to run a piece of code on a timed interval, or want to wait +a few seconds, you don't have to rely on timing tricks, as Hilbish has a +timer API to set intervals and timeouts. + +These are the simple functions `hilbish.interval` and `hilbish.timeout` (doc +accessible with `doc hilbish`). But if you want slightly more control over +them, there is the `hilbish.timers` interface. It allows you to get +a timer via ID. + +# Timer Interface +## Functions +- `get(id)` -> timer: get a timer via its id +- `create(type, ms, callback)` -> timer: creates a timer, adding it to the timer pool. +`type` is the type of timer it will be. 0 is an interval, 1 is a timeout. +`ms` is the time it will run for in seconds. callback is the function called +when the timer is triggered. + +# Timer Object +Those previously mentioned functions return a `timer` object, to which you can +stop and start a timer again. The functions of the timers interface also +return a timer object. + +## Properties +- `duration`: amount of time the timer runs for in milliseconds +- `running`: whether the timer is running or not +- `type`: the type of timer (0 is interval, 1 is timeout) + +## Functions +- `stop()`: stops the timer. returns an error if it's already stopped +- `start()`: starts the timer. returns an error if it's already started diff --git a/main.go b/main.go index a3a21c3..7106305 100644 --- a/main.go +++ b/main.go @@ -145,6 +145,7 @@ func main() { text := scanner.Text() runInput(text, true) } + exit(0) } if *cmdflag != "" { @@ -161,9 +162,9 @@ func main() { err := util.DoFile(l, getopt.Arg(0)) if err != nil { fmt.Fprintln(os.Stderr, err) - os.Exit(1) + exit(1) } - os.Exit(0) + exit(0) } initialized = true @@ -299,3 +300,12 @@ func contains(s []string, e string) bool { } return false } + +func exit(code int) { + // wait for all timers to finish before exiting + for { + if timers.running == 0 { + os.Exit(code) + } + } +} diff --git a/timer.go b/timer.go new file mode 100644 index 0000000..481156d --- /dev/null +++ b/timer.go @@ -0,0 +1,106 @@ +package main + +import ( + "errors" + "fmt" + "os" + "time" + + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" +) + +type timerType int64 +const ( + timerInterval timerType = iota + timerTimeout +) + +type timer struct{ + id int + typ timerType + running bool + dur time.Duration + fun *rt.Closure + th *timerHandler + ticker *time.Ticker + channel chan bool +} + +func (t *timer) start() error { + if t.running { + return errors.New("timer is already running") + } + + t.running = true + t.th.running++ + t.ticker = time.NewTicker(t.dur) + + go func() { + for { + select { + case <-t.ticker.C: + _, err := rt.Call1(l.MainThread(), rt.FunctionValue(t.fun)) + if err != nil { + fmt.Fprintln(os.Stderr, "Error in function:\n", err) + t.stop() + } + // only run one for timeout + if t.typ == timerTimeout { + t.stop() + } + case <-t.channel: + t.ticker.Stop() + return + } + } + }() + + return nil +} + +func (t *timer) stop() error { + if !t.running { + return errors.New("timer not running") + } + + t.channel <- true + t.running = false + t.th.running-- + + return nil +} + +func (t *timer) luaStart(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + err := t.start() + if err != nil { + return nil, err + } + + return c.Next(), nil +} + +func (t *timer) luaStop(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + err := t.stop() + if err != nil { + return nil, err + } + + return c.Next(), nil +} + +func (t *timer) lua() rt.Value { + tExports := map[string]util.LuaExport{ + "start": {t.luaStart, 0, false}, + "stop": {t.luaStop, 0, false}, + } + luaTimer := rt.NewTable() + util.SetExports(l, luaTimer, tExports) + + luaTimer.Set(rt.StringValue("type"), rt.IntValue(int64(t.typ))) + luaTimer.Set(rt.StringValue("running"), rt.BoolValue(t.running)) + luaTimer.Set(rt.StringValue("duration"), rt.IntValue(int64(t.dur / time.Millisecond))) + + return rt.TableValue(luaTimer) +} diff --git a/timerhandler.go b/timerhandler.go new file mode 100644 index 0000000..a85bb17 --- /dev/null +++ b/timerhandler.go @@ -0,0 +1,102 @@ +package main + +import ( + "sync" + "time" + + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" +) + +var timers *timerHandler +type timerHandler struct { + mu *sync.RWMutex + timers map[int]*timer + latestID int + running int +} + +func newTimerHandler() *timerHandler { + return &timerHandler{ + timers: make(map[int]*timer), + latestID: 0, + mu: &sync.RWMutex{}, + } +} + +func (th *timerHandler) create(typ timerType, dur time.Duration, fun *rt.Closure) *timer { + th.mu.Lock() + defer th.mu.Unlock() + + th.latestID++ + t := &timer{ + typ: typ, + fun: fun, + dur: dur, + channel: make(chan bool, 1), + th: th, + id: th.latestID, + } + th.timers[th.latestID] = t + + return t +} + +func (th *timerHandler) get(id int) *timer { + th.mu.RLock() + defer th.mu.RUnlock() + + return th.timers[id] +} + +func (th *timerHandler) luaCreate(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(3); err != nil { + return nil, err + } + timerTypInt, err := c.IntArg(0) + if err != nil { + return nil, err + } + ms, err := c.IntArg(1) + if err != nil { + return nil, err + } + cb, err := c.ClosureArg(2) + if err != nil { + return nil, err + } + + timerTyp := timerType(timerTypInt) + tmr := th.create(timerTyp, time.Duration(ms) * time.Millisecond, cb) + return c.PushingNext1(t.Runtime, tmr.lua()), nil +} + +func (th *timerHandler) luaGet(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + id, err := c.IntArg(0) + if err != nil { + return nil, err + } + + t := th.get(int(id)) + if t != nil { + return c.PushingNext1(thr.Runtime, t.lua()), nil + } + + return c.Next(), nil +} + +func (th *timerHandler) loader(rtm *rt.Runtime) *rt.Table { + thExports := map[string]util.LuaExport{ + "create": {th.luaCreate, 3, false}, + "get": {th.luaGet, 1, false}, + } + + luaTh := rt.NewTable() + util.SetExports(rtm, luaTh, thExports) + + return luaTh +}