From 029492e16d9487e0c274f00092bce1c36b673add Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 13:45:54 +0100 Subject: [PATCH] [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 --- bumblebee/input.py | 8 ++++++++ tests/modules/test_cmus.py | 5 ++++- tests/modules/test_cpu.py | 4 +++- tests/test_i3barinput.py | 40 ++++++++++++++++++++++++++++---------- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/bumblebee/input.py b/bumblebee/input.py index 76b9bf6..a501a85 100644 --- a/bumblebee/input.py +++ b/bumblebee/input.py @@ -4,6 +4,7 @@ import sys import json import uuid import time +import select import threading import bumblebee.util @@ -15,6 +16,13 @@ WHEEL_DOWN = 5 def read_input(inp): """Read i3bar input and execute callbacks""" 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() inp.has_event = True try: diff --git a/tests/modules/test_cmus.py b/tests/modules/test_cmus.py index f31b65a..1bc8b67 100644 --- a/tests/modules/test_cmus.py +++ b/tests/modules/test_cmus.py @@ -29,9 +29,10 @@ class TestCmusModule(unittest.TestCase): def test_widgets(self): self.assertTrue(len(self.module.widgets()), 5) + @mock.patch("select.select") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") - def test_interaction(self, mock_input, mock_output): + def test_interaction(self, mock_input, mock_output, mock_select): events = [ {"widget": "cmus.shuffle", "action": "cmus-remote -S"}, {"widget": "cmus.repeat", "action": "cmus-remote -R"}, @@ -40,6 +41,8 @@ class TestCmusModule(unittest.TestCase): {"widget": "cmus.main", "action": "cmus-remote -u"}, ] + mock_select.return_value = (1,2,3) + for event in events: mock_input.readline.return_value = json.dumps({ "name": self.module.id, diff --git a/tests/modules/test_cpu.py b/tests/modules/test_cpu.py index 6793b83..52b7967 100644 --- a/tests/modules/test_cpu.py +++ b/tests/modules/test_cpu.py @@ -24,14 +24,16 @@ class TestCPUModule(unittest.TestCase): for widget in self.module.widgets(): self.assertEquals(len(widget.full_text()), len("100.00%")) + @mock.patch("select.select") @mock.patch("subprocess.Popen") @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({ "name": self.module.id, "button": bumblebee.input.LEFT_MOUSE, "instance": None }) + mock_select.return_value = (1,2,3) self.engine.input.start() self.engine.input.stop() mock_input.readline.assert_any_call() diff --git a/tests/test_i3barinput.py b/tests/test_i3barinput.py index 1ae2864..57aa161 100644 --- a/tests/test_i3barinput.py +++ b/tests/test_i3barinput.py @@ -21,23 +21,29 @@ class TestI3BarInput(unittest.TestCase): def callback(self, event): self._called += 1 + @mock.patch("select.select") @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 = "" self.input.start() self.input.stop() mock_input.readline.assert_any_call() + @mock.patch("select.select") @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" self.input.start() self.assertEquals(self.input.alive(), True) self.assertEquals(self.input.stop(), True) mock_input.readline.assert_any_call() + @mock.patch("select.select") @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({ "name": None, "instance": None, @@ -48,8 +54,10 @@ class TestI3BarInput(unittest.TestCase): self.assertEquals(self.input.stop(), True) mock_input.readline.assert_any_call() + @mock.patch("select.select") @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({ "name": "somename", "instance": "someinstance", @@ -61,8 +69,10 @@ class TestI3BarInput(unittest.TestCase): mock_input.readline.assert_any_call() self.assertTrue(self._called > 0) + @mock.patch("select.select") @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({ "name": "somename", "instance": "someinstance", @@ -75,8 +85,10 @@ class TestI3BarInput(unittest.TestCase): mock_input.readline.assert_any_call() self.assertTrue(self._called == 0) + @mock.patch("select.select") @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({ "name": "somename", "instance": "someinstance", @@ -88,8 +100,10 @@ class TestI3BarInput(unittest.TestCase): mock_input.readline.assert_any_call() self.assertTrue(self._called == 0) + @mock.patch("select.select") @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({ "name": self.anyModule.id, "instance": None, @@ -101,8 +115,10 @@ class TestI3BarInput(unittest.TestCase): mock_input.readline.assert_any_call() self.assertTrue(self._called > 0) + @mock.patch("select.select") @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({ "name": self.anyModule.id, "instance": None, @@ -115,8 +131,10 @@ class TestI3BarInput(unittest.TestCase): mock_input.readline.assert_any_call() self.assertTrue(self._called == 0) + @mock.patch("select.select") @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({ "name": "test", "instance": self.anyWidget.id, @@ -128,9 +146,11 @@ class TestI3BarInput(unittest.TestCase): mock_input.readline.assert_any_call() self.assertTrue(self._called > 0) + @mock.patch("select.select") @mock.patch("subprocess.Popen") @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({ "name": "test", "instance": self.anyWidget.id,