Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add alternative PollEvents() impl to prevent goroutine leaking #273

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions v3/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package termui

import (
"context"
"fmt"

tb "github.com/nsf/termbox-go"
Expand Down Expand Up @@ -77,6 +78,23 @@ func PollEvents() <-chan Event {
return ch
}

// PollEventsWithContext same as PollEvents() prevents goroutine leaking using context.
func PollEventsWithContext(ctx context.Context) <-chan Event {
ch := make(chan Event)
go func() {
defer close(ch)
for {
select {
case <-ctx.Done():
return
default:
ch <- convertTermboxEvent(tb.PollEvent())
Comment on lines +90 to +91
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
default:
ch <- convertTermboxEvent(tb.PollEvent())
case ch <- convertTermboxEvent(tb.PollEvent()):

is better, otherwise while you are waiting for ch input, cancelling the context is not efective.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maxatome The core problem is that tb.PollEvent() is blocking.
This suggestion would not fix that what you claim as it would just handle a cancel during the write on ch after an event has been blockingly read.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maxatome The core problem is that tb.PollEvent() is blocking.
This suggestion would not fix that what you claim as it would just handle a cancel during the write on ch after an event has been blockingly read.

Yes, I had this in mind.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any suggestions how to make it better?

Copy link

@maxatome maxatome Nov 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point @dolmen :)

What about a specific goroutine to handle this blocking function?

Something like that (not tested in termui context):

func PollEventsWithContext(ctx context.Context) <-chan Event {
	// tb.PollEvent() is a blocking function, so dedicate a goroutine for it
	tbEvent := make(chan Event)
	go func() {
		defer close(tbEvent)
		for {
			ev := tb.PollEvent()
			if ev.Type == tb.EventInterrupt {
				return
			}
			tbEvent <- convertTermboxEvent(ev)
		}
	}()

	ch := make(chan Event)
	go func() {
		defer close(ch)
		ctxDone := ctx.Done()
		for {
			select {
			case ctxDone:
				// interrupt tb.PollEvent(), this call blocks until the
				// PollEvent function has successfully been interrupted
				tb.Interrupt()
				ctxDone = nil
			case ev, ok := <-tbEvent:
				// Successfully interrupted
				if !ok {
					return
				}
				ch <- ev
			}
		}
	}()
	return ch
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should works, however it looks like over usage of go-routines to me. I would like to suggest a simpler way to do it:

func PollEventsWithCancel() (<-chan Event, func()) {
	ch := make(chan Event)
	go func() {
		defer close(ch)
		for {
			ev := tb.PollEvent()
			if ev.Type == tb.EventInterrupt {
				return
			}
			ch <- convertTermboxEvent(ev)
		}
	}()
	return ch, func(){tb.Interrupt()}
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another concern is that the method should be a singleton. In other way behavior is unpredictable and doesn't really make sense.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me back to my pet proj after a while and see what can be done, any suggestions are welcome.

}
}
}()
return ch
}

var keyboardMap = map[tb.Key]string{
tb.KeyF1: "<F1>",
tb.KeyF2: "<F2>",
Expand Down