[core] Non-blocking input thread for i3bar events

Make input thread non-blocking by using select(). This increases the CPU
utilization a bit (depending on the timeout), but makes the thread exit
cleanly, even if an exception is thrown in the main thread.

see #23
This commit is contained in:
Tobi-wan Kenobi 2016-12-10 13:45:54 +01:00
parent 0489ce1b51
commit 029492e16d
4 changed files with 45 additions and 12 deletions

View file

@ -4,6 +4,7 @@ import sys
import json import json
import uuid import uuid
import time import time
import select
import threading import threading
import bumblebee.util import bumblebee.util
@ -15,6 +16,13 @@ WHEEL_DOWN = 5
def read_input(inp): def read_input(inp):
"""Read i3bar input and execute callbacks""" """Read i3bar input and execute callbacks"""
while inp.running: while inp.running:
for thread in threading.enumerate():
if thread.name == "MainThread" and not thread.is_alive():
return
rlist, _, _ = select.select([sys.stdin], [], [], 1)
if not rlist:
continue
line = sys.stdin.readline().strip(",").strip() line = sys.stdin.readline().strip(",").strip()
inp.has_event = True inp.has_event = True
try: try:

View file

@ -29,9 +29,10 @@ class TestCmusModule(unittest.TestCase):
def test_widgets(self): def test_widgets(self):
self.assertTrue(len(self.module.widgets()), 5) self.assertTrue(len(self.module.widgets()), 5)
@mock.patch("select.select")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_interaction(self, mock_input, mock_output): def test_interaction(self, mock_input, mock_output, mock_select):
events = [ events = [
{"widget": "cmus.shuffle", "action": "cmus-remote -S"}, {"widget": "cmus.shuffle", "action": "cmus-remote -S"},
{"widget": "cmus.repeat", "action": "cmus-remote -R"}, {"widget": "cmus.repeat", "action": "cmus-remote -R"},
@ -40,6 +41,8 @@ class TestCmusModule(unittest.TestCase):
{"widget": "cmus.main", "action": "cmus-remote -u"}, {"widget": "cmus.main", "action": "cmus-remote -u"},
] ]
mock_select.return_value = (1,2,3)
for event in events: for event in events:
mock_input.readline.return_value = json.dumps({ mock_input.readline.return_value = json.dumps({
"name": self.module.id, "name": self.module.id,

View file

@ -24,14 +24,16 @@ class TestCPUModule(unittest.TestCase):
for widget in self.module.widgets(): for widget in self.module.widgets():
self.assertEquals(len(widget.full_text()), len("100.00%")) self.assertEquals(len(widget.full_text()), len("100.00%"))
@mock.patch("select.select")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_leftclick(self, mock_input, mock_output): def test_leftclick(self, mock_input, mock_output, mock_select):
mock_input.readline.return_value = json.dumps({ mock_input.readline.return_value = json.dumps({
"name": self.module.id, "name": self.module.id,
"button": bumblebee.input.LEFT_MOUSE, "button": bumblebee.input.LEFT_MOUSE,
"instance": None "instance": None
}) })
mock_select.return_value = (1,2,3)
self.engine.input.start() self.engine.input.start()
self.engine.input.stop() self.engine.input.stop()
mock_input.readline.assert_any_call() mock_input.readline.assert_any_call()

View file

@ -21,23 +21,29 @@ class TestI3BarInput(unittest.TestCase):
def callback(self, event): def callback(self, event):
self._called += 1 self._called += 1
@mock.patch("select.select")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_basic_read_event(self, mock_input): def test_basic_read_event(self, mock_input, mock_select):
mock_select.return_value = (1,2,3)
mock_input.readline.return_value = "" mock_input.readline.return_value = ""
self.input.start() self.input.start()
self.input.stop() self.input.stop()
mock_input.readline.assert_any_call() mock_input.readline.assert_any_call()
@mock.patch("select.select")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_ignore_invalid_data(self, mock_input): def test_ignore_invalid_data(self, mock_input, mock_select):
mock_select.return_value = (1,2,3)
mock_input.readline.return_value = "garbage" mock_input.readline.return_value = "garbage"
self.input.start() self.input.start()
self.assertEquals(self.input.alive(), True) self.assertEquals(self.input.alive(), True)
self.assertEquals(self.input.stop(), True) self.assertEquals(self.input.stop(), True)
mock_input.readline.assert_any_call() mock_input.readline.assert_any_call()
@mock.patch("select.select")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_ignore_invalid_event(self, mock_input): def test_ignore_invalid_event(self, mock_input, mock_select):
mock_select.return_value = (1,2,3)
mock_input.readline.return_value = json.dumps({ mock_input.readline.return_value = json.dumps({
"name": None, "name": None,
"instance": None, "instance": None,
@ -48,8 +54,10 @@ class TestI3BarInput(unittest.TestCase):
self.assertEquals(self.input.stop(), True) self.assertEquals(self.input.stop(), True)
mock_input.readline.assert_any_call() mock_input.readline.assert_any_call()
@mock.patch("select.select")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_global_callback(self, mock_input): def test_global_callback(self, mock_input, mock_select):
mock_select.return_value = (1,2,3)
mock_input.readline.return_value = json.dumps({ mock_input.readline.return_value = json.dumps({
"name": "somename", "name": "somename",
"instance": "someinstance", "instance": "someinstance",
@ -61,8 +69,10 @@ class TestI3BarInput(unittest.TestCase):
mock_input.readline.assert_any_call() mock_input.readline.assert_any_call()
self.assertTrue(self._called > 0) self.assertTrue(self._called > 0)
@mock.patch("select.select")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_remove_global_callback(self, mock_input): def test_remove_global_callback(self, mock_input, mock_select):
mock_select.return_value = (1,2,3)
mock_input.readline.return_value = json.dumps({ mock_input.readline.return_value = json.dumps({
"name": "somename", "name": "somename",
"instance": "someinstance", "instance": "someinstance",
@ -75,8 +85,10 @@ class TestI3BarInput(unittest.TestCase):
mock_input.readline.assert_any_call() mock_input.readline.assert_any_call()
self.assertTrue(self._called == 0) self.assertTrue(self._called == 0)
@mock.patch("select.select")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_global_callback_button_missmatch(self, mock_input): def test_global_callback_button_missmatch(self, mock_input, mock_select):
mock_select.return_value = (1,2,3)
mock_input.readline.return_value = json.dumps({ mock_input.readline.return_value = json.dumps({
"name": "somename", "name": "somename",
"instance": "someinstance", "instance": "someinstance",
@ -88,8 +100,10 @@ class TestI3BarInput(unittest.TestCase):
mock_input.readline.assert_any_call() mock_input.readline.assert_any_call()
self.assertTrue(self._called == 0) self.assertTrue(self._called == 0)
@mock.patch("select.select")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_module_callback(self, mock_input): def test_module_callback(self, mock_input, mock_select):
mock_select.return_value = (1,2,3)
mock_input.readline.return_value = json.dumps({ mock_input.readline.return_value = json.dumps({
"name": self.anyModule.id, "name": self.anyModule.id,
"instance": None, "instance": None,
@ -101,8 +115,10 @@ class TestI3BarInput(unittest.TestCase):
mock_input.readline.assert_any_call() mock_input.readline.assert_any_call()
self.assertTrue(self._called > 0) self.assertTrue(self._called > 0)
@mock.patch("select.select")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_remove_module_callback(self, mock_input): def test_remove_module_callback(self, mock_input, mock_select):
mock_select.return_value = (1,2,3)
mock_input.readline.return_value = json.dumps({ mock_input.readline.return_value = json.dumps({
"name": self.anyModule.id, "name": self.anyModule.id,
"instance": None, "instance": None,
@ -115,8 +131,10 @@ class TestI3BarInput(unittest.TestCase):
mock_input.readline.assert_any_call() mock_input.readline.assert_any_call()
self.assertTrue(self._called == 0) self.assertTrue(self._called == 0)
@mock.patch("select.select")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_widget_callback(self, mock_input): def test_widget_callback(self, mock_input, mock_select):
mock_select.return_value = (1,2,3)
mock_input.readline.return_value = json.dumps({ mock_input.readline.return_value = json.dumps({
"name": "test", "name": "test",
"instance": self.anyWidget.id, "instance": self.anyWidget.id,
@ -128,9 +146,11 @@ class TestI3BarInput(unittest.TestCase):
mock_input.readline.assert_any_call() mock_input.readline.assert_any_call()
self.assertTrue(self._called > 0) self.assertTrue(self._called > 0)
@mock.patch("select.select")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_widget_cmd_callback(self, mock_input, mock_output): def test_widget_cmd_callback(self, mock_input, mock_output, mock_select):
mock_select.return_value = (1,2,3)
mock_input.readline.return_value = json.dumps({ mock_input.readline.return_value = json.dumps({
"name": "test", "name": "test",
"instance": self.anyWidget.id, "instance": self.anyWidget.id,