refactor: use custom event emitter (#193)

* refactor: use custom event emitter

* fix: sigint hook emit on windows

* fix: restore correct hilbish conf file

* fix: call recoverer for go listeners

* refactor(golibs/bait): use 1 map for listeners

* feat: add once listeners, ability to remove listeners and remove listener on error

* perf: reslice listener slice instead of trying to do ordered move with append

* feat(bait): add release function to remove event listener

* perf: remove listener directly from once emit instead of using off function

* refactor: use bait event emitter on commander

* docs(golibs/bait): add doc strings for functions

* docs: set changelog

* docs(golibs/bait): add docs for lua release function
cancel-commander
sammy 2022-08-17 18:01:32 -04:00 committed by GitHub
parent 6ce4fb3973
commit 2337f9ab60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 226 additions and 49 deletions

View File

@ -70,6 +70,11 @@ set `hilbish.opts.motd` to false.
disables commands being added to history. disables commands being added to history.
- `hilbish.rawInput` hook for input from the readline library - `hilbish.rawInput` hook for input from the readline library
- Completion of files in quotes - Completion of files in quotes
- A new and "safer" event emitter has been added. This causes a performance deficit, but avoids a lot of
random errors introduced with the new Lua runtime (see [#197])
- `bait.release(name, catcher)` removes `handler` for the named `event`
[#197]: https://github.com/Rosettea/Hilbish/issues/197
### Changed ### Changed
- **Breaking Change:** Upgraded to Lua 5.4. - **Breaking Change:** Upgraded to Lua 5.4.

2
api.go
View File

@ -189,7 +189,7 @@ func getenv(key, fallback string) string {
func setVimMode(mode string) { func setVimMode(mode string) {
util.SetField(l, hshMod, "vimMode", rt.StringValue(mode), "Current Vim mode of Hilbish (nil if not in Vim mode)") util.SetField(l, hshMod, "vimMode", rt.StringValue(mode), "Current Vim mode of Hilbish (nil if not in Vim mode)")
hooks.Em.Emit("hilbish.vimMode", mode) hooks.Emit("hilbish.vimMode", mode)
} }
func unsetVimMode() { func unsetVimMode() {

View File

@ -85,7 +85,7 @@ func isExecError(err error) (execError, bool) {
func runInput(input string, priv bool) { func runInput(input string, priv bool) {
running = true running = true
cmdString := aliases.Resolve(input) cmdString := aliases.Resolve(input)
hooks.Em.Emit("command.preexec", input, cmdString) hooks.Emit("command.preexec", input, cmdString)
rerun: rerun:
var exitCode uint8 var exitCode uint8
@ -140,7 +140,7 @@ func runInput(input string, priv bool) {
if err != nil { if err != nil {
if exErr, ok := isExecError(err); ok { if exErr, ok := isExecError(err); ok {
hooks.Em.Emit("command." + exErr.typ, exErr.cmd) hooks.Emit("command." + exErr.typ, exErr.cmd)
err = exErr.sprint() err = exErr.sprint()
} }
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
@ -544,5 +544,5 @@ func cmdFinish(code uint8, cmdstr string, private bool) {
// using AsValue (to convert to lua type) on an interface which is an int // using AsValue (to convert to lua type) on an interface which is an int
// results in it being unknown in lua .... ???? // results in it being unknown in lua .... ????
// so we allow the hook handler to take lua runtime Values // so we allow the hook handler to take lua runtime Values
hooks.Em.Emit("command.exit", rt.IntValue(int64(code)), cmdstr, private) hooks.Emit("command.exit", rt.IntValue(int64(code)), cmdstr, private)
} }

View File

@ -1,27 +1,41 @@
package bait package bait
import ( import (
"fmt"
"hilbish/util" "hilbish/util"
rt "github.com/arnodel/golua/runtime" rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib" "github.com/arnodel/golua/lib/packagelib"
"github.com/chuckpreslar/emission"
) )
type Bait struct{ type listenerType int
Em *emission.Emitter const (
Loader packagelib.Loader goListener listenerType = iota
luaListener
)
// Recoverer is a function which is called when a panic occurs in an event.
type Recoverer func(event string, handler *Listener, err interface{})
// Listener is a struct that holds the handler for an event.
type Listener struct{
typ listenerType
once bool
caller func(...interface{})
luaCaller *rt.Closure
} }
func New() Bait { type Bait struct{
emitter := emission.NewEmitter() Loader packagelib.Loader
emitter.RecoverWith(func(hookname, hookfunc interface{}, err error) { recoverer Recoverer
emitter.Off(hookname, hookfunc) handlers map[string][]*Listener
fmt.Println(err) rtm *rt.Runtime
}) }
b := Bait{
Em: emitter, // New creates a new Bait instance.
func New(rtm *rt.Runtime) *Bait {
b := &Bait{
handlers: make(map[string][]*Listener),
rtm: rtm,
} }
b.Loader = packagelib.Loader{ b.Loader = packagelib.Loader{
Load: b.loaderFunc, Load: b.loaderFunc,
@ -31,11 +45,148 @@ func New() Bait {
return b return b
} }
// Emit throws an event.
func (b *Bait) Emit(event string, args ...interface{}) {
handles := b.handlers[event]
if handles == nil {
return
}
for idx, handle := range handles {
defer func() {
if err := recover(); err != nil {
b.callRecoverer(event, handle, err)
}
}()
if handle.typ == luaListener {
funcVal := rt.FunctionValue(handle.luaCaller)
var luaArgs []rt.Value
for _, arg := range args {
var luarg rt.Value
switch arg.(type) {
case rt.Value: luarg = arg.(rt.Value)
default: luarg = rt.AsValue(arg)
}
luaArgs = append(luaArgs, luarg)
}
_, err := rt.Call1(b.rtm.MainThread(), funcVal, luaArgs...)
if err != nil {
// panicking here won't actually cause hilbish to panic and instead will
// print the error and remove the hook. reference the recoverer function in lua.go
panic(err)
}
} else {
handle.caller(args...)
}
if handle.once {
b.removeListener(event, idx)
}
}
}
// On adds a Go function handler for an event.
func (b *Bait) On(event string, handler func(...interface{})) *Listener {
listener := &Listener{
typ: goListener,
caller: handler,
}
b.addListener(event, listener)
return listener
}
// OnLua adds a Lua function handler for an event.
func (b *Bait) OnLua(event string, handler *rt.Closure) *Listener {
listener :=&Listener{
typ: luaListener,
luaCaller: handler,
}
b.addListener(event, listener)
return listener
}
// Off removes a Go function handler for an event.
func (b *Bait) Off(event string, listener *Listener) {
handles := b.handlers[event]
for i, handle := range handles {
if handle == listener {
b.removeListener(event, i)
}
}
}
// OffLua removes a Lua function handler for an event.
func (b *Bait) OffLua(event string, handler *rt.Closure) {
handles := b.handlers[event]
for i, handle := range handles {
if handle.luaCaller == handler {
b.removeListener(event, i)
}
}
}
// Once adds a Go function listener for an event that only runs once.
func (b *Bait) Once(event string, handler func(...interface{})) *Listener {
listener := &Listener{
typ: goListener,
once: true,
caller: handler,
}
b.addListener(event, listener)
return listener
}
// OnceLua adds a Lua function listener for an event that only runs once.
func (b *Bait) OnceLua(event string, handler *rt.Closure) *Listener {
listener := &Listener{
typ: luaListener,
once: true,
luaCaller: handler,
}
b.addListener(event, listener)
return listener
}
// SetRecoverer sets the function to be executed when a panic occurs in an event.
func (b *Bait) SetRecoverer(recoverer Recoverer) {
b.recoverer = recoverer
}
func (b *Bait) addListener(event string, listener *Listener) {
if b.handlers[event] == nil {
b.handlers[event] = []*Listener{}
}
b.handlers[event] = append(b.handlers[event], listener)
}
func (b *Bait) removeListener(event string, idx int) {
b.handlers[event][idx] = b.handlers[event][len(b.handlers[event]) - 1]
b.handlers[event] = b.handlers[event][:len(b.handlers[event]) - 1]
}
func (b *Bait) callRecoverer(event string, handler *Listener, err interface{}) {
if b.recoverer == nil {
panic(err)
}
b.recoverer(event, handler, err)
}
func (b *Bait) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { func (b *Bait) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
exports := map[string]util.LuaExport{ exports := map[string]util.LuaExport{
"catch": util.LuaExport{b.bcatch, 2, false}, "catch": util.LuaExport{b.bcatch, 2, false},
"catchOnce": util.LuaExport{b.bcatchOnce, 2, false}, "catchOnce": util.LuaExport{b.bcatchOnce, 2, false},
"throw": util.LuaExport{b.bthrow, 1, true}, "throw": util.LuaExport{b.bthrow, 1, true},
"release": util.LuaExport{b.brelease, 2, false},
} }
mod := rt.NewTable() mod := rt.NewTable()
util.SetExports(rtm, mod, exports) util.SetExports(rtm, mod, exports)
@ -89,7 +240,7 @@ func (b *Bait) bthrow(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
for i, v := range c.Etc() { for i, v := range c.Etc() {
ifaceSlice[i] = v ifaceSlice[i] = v
} }
b.Em.Emit(name, ifaceSlice...) b.Emit(name, ifaceSlice...)
return c.Next(), nil return c.Next(), nil
} }
@ -104,9 +255,7 @@ func (b *Bait) bcatch(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return nil, err return nil, err
} }
b.Em.On(name, func(args ...interface{}) { b.OnLua(name, catcher)
handleHook(t, c, name, catcher, args...)
})
return c.Next(), nil return c.Next(), nil
} }
@ -121,9 +270,22 @@ func (b *Bait) bcatchOnce(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return nil, err return nil, err
} }
b.Em.Once(name, func(args ...interface{}) { b.OnceLua(name, catcher)
handleHook(t, c, name, catcher, args...)
}) return c.Next(), nil
}
// release(name, catcher)
// Removes the `catcher` for the event with `name`
// For this to work, `catcher` has to be the same function used to catch
// an event, like one saved to a variable.
func (b *Bait) brelease(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
name, catcher, err := util.HandleStrCallback(t, c)
if err != nil {
return nil, err
}
b.OffLua(name, catcher)
return c.Next(), nil return c.Next(), nil
} }

View File

@ -2,20 +2,20 @@ package commander
import ( import (
"hilbish/util" "hilbish/util"
"hilbish/golibs/bait"
rt "github.com/arnodel/golua/runtime" rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib" "github.com/arnodel/golua/lib/packagelib"
"github.com/chuckpreslar/emission"
) )
type Commander struct{ type Commander struct{
Events *emission.Emitter Events *bait.Bait
Loader packagelib.Loader Loader packagelib.Loader
} }
func New() Commander { func New(rtm *rt.Runtime) Commander {
c := Commander{ c := Commander{
Events: emission.NewEmitter(), Events: bait.New(rtm),
} }
c.Loader = packagelib.Loader{ c.Loader = packagelib.Loader{
Load: c.loaderFunc, Load: c.loaderFunc,

6
job.go
View File

@ -67,7 +67,7 @@ func (j *job) start() error {
j.pid = proc.Pid j.pid = proc.Pid
j.running = true j.running = true
hooks.Em.Emit("job.start", rt.UserDataValue(j.ud)) hooks.Emit("job.start", rt.UserDataValue(j.ud))
return err return err
} }
@ -82,7 +82,7 @@ func (j *job) stop() {
func (j *job) finish() { func (j *job) finish() {
j.running = false j.running = false
hooks.Em.Emit("job.done", rt.UserDataValue(j.ud)) hooks.Emit("job.done", rt.UserDataValue(j.ud))
} }
func (j *job) wait() { func (j *job) wait() {
@ -236,7 +236,7 @@ func (j *jobHandler) add(cmd string, args []string, path string) *job {
jb.ud = jobUserData(jb) jb.ud = jobUserData(jb)
j.jobs[j.latestID] = jb j.jobs[j.latestID] = jb
hooks.Em.Emit("job.add", rt.UserDataValue(jb.ud)) hooks.Emit("job.add", rt.UserDataValue(jb.ud))
return jb return jb
} }

22
lua.go
View File

@ -32,28 +32,38 @@ func luaInit() {
lib.LoadLibs(l, fs.Loader) lib.LoadLibs(l, fs.Loader)
lib.LoadLibs(l, terminal.Loader) lib.LoadLibs(l, terminal.Loader)
cmds := commander.New() cmds := commander.New(l)
// When a command from Lua is added, register it for use // When a command from Lua is added, register it for use
cmds.Events.On("commandRegister", func(cmdName string, cmd *rt.Closure) { cmds.Events.On("commandRegister", func(args ...interface{}) {
cmdName := args[0].(string)
cmd := args[1].(*rt.Closure)
commands[cmdName] = cmd commands[cmdName] = cmd
}) })
cmds.Events.On("commandDeregister", func(cmdName string) { cmds.Events.On("commandDeregister", func(args ...interface{}) {
cmdName := args[0].(string)
delete(commands, cmdName) delete(commands, cmdName)
}) })
lib.LoadLibs(l, cmds.Loader) lib.LoadLibs(l, cmds.Loader)
hooks = bait.New() hooks = bait.New(l)
hooks.SetRecoverer(func(event string, handler *bait.Listener, err interface{}) {
fmt.Println("Error in", event, "event:", err)
hooks.Off(event, handler)
})
lib.LoadLibs(l, hooks.Loader) lib.LoadLibs(l, hooks.Loader)
// Add Ctrl-C handler // Add Ctrl-C handler
hooks.Em.On("signal.sigint", func() { hooks.On("signal.sigint", func(...interface{}) {
if !interactive { if !interactive {
os.Exit(0) os.Exit(0)
} }
}) })
lr.rl.RawInputCallback = func(r []rune) { lr.rl.RawInputCallback = func(r []rune) {
hooks.Em.Emit("hilbish.rawInput", string(r)) hooks.Emit("hilbish.rawInput", string(r))
} }
// Add more paths that Lua can require from // Add more paths that Lua can require from

10
main.go
View File

@ -30,7 +30,7 @@ var (
userDataDir string userDataDir string
curuser *user.User curuser *user.User
hooks bait.Bait hooks *bait.Bait
defaultConfPath string defaultConfPath string
defaultHistPath string defaultHistPath string
) )
@ -138,7 +138,7 @@ func main() {
} else { } else {
runConfig(*configflag) runConfig(*configflag)
} }
hooks.Em.Emit("hilbish.init") hooks.Emit("hilbish.init")
if fileInfo, _ := os.Stdin.Stat(); (fileInfo.Mode() & os.ModeCharDevice) == 0 { if fileInfo, _ := os.Stdin.Stat(); (fileInfo.Mode() & os.ModeCharDevice) == 0 {
scanner := bufio.NewScanner(bufio.NewReader(os.Stdin)) scanner := bufio.NewScanner(bufio.NewReader(os.Stdin))
@ -177,7 +177,7 @@ input:
if err == io.EOF { if err == io.EOF {
// Exit if user presses ^D (ctrl + d) // Exit if user presses ^D (ctrl + d)
hooks.Em.Emit("hilbish.exit") hooks.Emit("hilbish.exit")
break break
} }
if err != nil { if err != nil {
@ -196,7 +196,7 @@ input:
input = strings.TrimSpace(input) input = strings.TrimSpace(input)
if len(input) == 0 { if len(input) == 0 {
running = true running = true
hooks.Em.Emit("command.exit", 0) hooks.Emit("command.exit", 0)
continue continue
} }
@ -227,7 +227,7 @@ input:
} }
func continuePrompt(prev string) (string, error) { func continuePrompt(prev string) (string, error) {
hooks.Em.Emit("multiline", nil) hooks.Emit("multiline", nil)
lr.SetPrompt(multilinePrompt) lr.SetPrompt(multilinePrompt)
cont, err := lr.Read() cont, err := lr.Read()
if err != nil { if err != nil {

4
rl.go
View File

@ -48,7 +48,7 @@ func newLineReader(prompt string, noHist bool) *lineReader {
case readline.VimActionPaste: actionStr = "paste" case readline.VimActionPaste: actionStr = "paste"
case readline.VimActionYank: actionStr = "yank" case readline.VimActionYank: actionStr = "yank"
} }
hooks.Em.Emit("hilbish.vimAction", actionStr, args) hooks.Emit("hilbish.vimAction", actionStr, args)
} }
rl.HintText = func(line []rune, pos int) []rune { rl.HintText = func(line []rune, pos int) []rune {
if hinter == nil { if hinter == nil {
@ -179,7 +179,7 @@ func newLineReader(prompt string, noHist bool) *lineReader {
} }
func (lr *lineReader) Read() (string, error) { func (lr *lineReader) Read() (string, error) {
hooks.Em.Emit("command.precmd", nil) hooks.Emit("command.precmd", nil)
s, err := lr.rl.Readline() s, err := lr.rl.Readline()
// this is so dumb // this is so dumb
if err == readline.EOF { if err == readline.EOF {

View File

@ -15,11 +15,11 @@ func handleSignals() {
for s := range c { for s := range c {
switch s { switch s {
case os.Interrupt: hooks.Em.Emit("signal.sigint") case os.Interrupt: hooks.Emit("signal.sigint")
case syscall.SIGTERM: exit(0) case syscall.SIGTERM: exit(0)
case syscall.SIGWINCH: hooks.Em.Emit("signal.resize") case syscall.SIGWINCH: hooks.Emit("signal.resize")
case syscall.SIGUSR1: hooks.Em.Emit("signal.sigusr1") case syscall.SIGUSR1: hooks.Emit("signal.sigusr1")
case syscall.SIGUSR2: hooks.Em.Emit("signal.sigusr2") case syscall.SIGUSR2: hooks.Emit("signal.sigusr2")
} }
} }
} }

View File

@ -14,7 +14,7 @@ func handleSignals() {
for s := range c { for s := range c {
switch s { switch s {
case os.Interrupt: case os.Interrupt:
hooks.Em.Emit("signal.sigint") hooks.Emit("signal.sigint")
if !running && interactive { if !running && interactive {
lr.ClearInput() lr.ClearInput()
} }