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 +}