[core/input] Move from select to epoll

Use epoll instead of select in order to be able to use level-triggered
semantics and not get stuck on the first event.
This commit is contained in:
Tobi-wan Kenobi 2016-12-17 07:43:38 +01:00
parent 31f9154be2
commit f6be25bc73
11 changed files with 67 additions and 48 deletions

View file

@ -16,24 +16,27 @@ WHEEL_DOWN = 5
def read_input(inp): def read_input(inp):
"""Read i3bar input and execute callbacks""" """Read i3bar input and execute callbacks"""
epoll = select.epoll()
epoll.register(sys.stdin.fileno(), select.EPOLLIN)
while inp.running: while inp.running:
for thread in threading.enumerate(): for thread in threading.enumerate():
if thread.name == "MainThread" and not thread.is_alive(): if thread.name == "MainThread" and not thread.is_alive():
return return
rlist, _, _ = select.select([sys.stdin], [], [], 1) events = epoll.poll(1)
if not rlist:
continue for fileno, event in events:
line = sys.stdin.readline().strip(",").strip() line = sys.stdin.readline().strip(",").strip()
inp.has_event = True inp.has_event = True
try: try:
event = json.loads(line) event = json.loads(line)
if not "instance" in event: if "instance" in event:
continue inp.callback(event)
inp.callback(event) inp.redraw()
inp.redraw() except ValueError:
except ValueError: pass
pass epoll.unregister(sys.stdin.fileno())
epoll.close()
inp.has_event = True inp.has_event = True
inp.clean_exit = True inp.clean_exit = True

View file

@ -25,7 +25,7 @@ def get_dnf_info(widget):
bugfixes = 0 bugfixes = 0
enhancements = 0 enhancements = 0
other = 0 other = 0
for line in res.decode().split("\n"): for line in res.split("\n"):
if not line.startswith(" "): continue if not line.startswith(" "): continue
elif "ecurity" in line: elif "ecurity" in line:

View file

@ -24,7 +24,7 @@ class TestBrightnessModule(unittest.TestCase):
for widget in self.module.widgets(): for widget in self.module.widgets():
self.assertEquals(len(widget.full_text()), len("100%")) self.assertEquals(len(widget.full_text()), len("100%"))
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_wheel_up(self, mock_input, mock_output, mock_select): def test_wheel_up(self, mock_input, mock_output, mock_select):
@ -33,7 +33,7 @@ class TestBrightnessModule(unittest.TestCase):
"xbacklight +2%" "xbacklight +2%"
) )
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_wheel_down(self, mock_input, mock_output, mock_select): def test_wheel_down(self, mock_input, mock_output, mock_select):
@ -42,7 +42,7 @@ class TestBrightnessModule(unittest.TestCase):
"xbacklight -2%" "xbacklight -2%"
) )
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_custom_step(self, mock_input, mock_output, mock_select): def test_custom_step(self, mock_input, mock_output, mock_select):

View file

@ -7,7 +7,7 @@ import mock
import bumblebee.input import bumblebee.input
from bumblebee.input import I3BarInput from bumblebee.input import I3BarInput
from bumblebee.modules.cmus import Module from bumblebee.modules.cmus import Module
from tests.util import MockEngine, MockConfig, assertPopen from tests.util import MockEngine, MockConfig, assertPopen, MockEpoll
class TestCmusModule(unittest.TestCase): class TestCmusModule(unittest.TestCase):
def setUp(self): def setUp(self):
@ -29,7 +29,7 @@ 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("select.epoll")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_interaction(self, mock_input, mock_output, mock_select): def test_interaction(self, mock_input, mock_output, mock_select):
@ -41,7 +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) mock_input.fileno.return_value = 1
mock_select.return_value = MockEpoll()
for event in events: for event in events:
mock_input.readline.return_value = json.dumps({ mock_input.readline.return_value = json.dumps({

View file

@ -22,7 +22,7 @@ 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("select.epoll")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_leftclick(self, mock_input, mock_output, mock_select): def test_leftclick(self, mock_input, mock_output, mock_select):

View file

@ -7,7 +7,7 @@ import mock
import bumblebee.input import bumblebee.input
from bumblebee.input import I3BarInput from bumblebee.input import I3BarInput
from bumblebee.modules.disk import Module from bumblebee.modules.disk import Module
from tests.util import MockEngine, MockConfig, assertPopen, assertStateContains from tests.util import MockEngine, MockConfig, assertPopen, assertStateContains, MockEpoll
class MockVFS(object): class MockVFS(object):
def __init__(self, perc): def __init__(self, perc):
@ -24,7 +24,7 @@ class TestDiskModule(unittest.TestCase):
self.config.set("disk.path", "somepath") self.config.set("disk.path", "somepath")
self.module = Module(engine=self.engine, config={"config": self.config}) self.module = Module(engine=self.engine, config={"config": self.config})
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_leftclick(self, mock_input, mock_output, mock_select): def test_leftclick(self, mock_input, mock_output, mock_select):
@ -33,7 +33,7 @@ class TestDiskModule(unittest.TestCase):
"button": bumblebee.input.LEFT_MOUSE, "button": bumblebee.input.LEFT_MOUSE,
"instance": None "instance": None
}) })
mock_select.return_value = (1,2,3) mock_select.return_value = MockEpoll()
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

@ -17,7 +17,7 @@ class TestLoadModule(unittest.TestCase):
self.config = MockConfig() self.config = MockConfig()
self.module = Module(engine=self.engine, config={ "config": self.config }) self.module = Module(engine=self.engine, config={ "config": self.config })
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_leftclick(self, mock_input, mock_output, mock_select): def test_leftclick(self, mock_input, mock_output, mock_select):

View file

@ -21,7 +21,7 @@ class TestMemoryModule(unittest.TestCase):
self.config = MockConfig() self.config = MockConfig()
self.module = Module(engine=self.engine, config={ "config": self.config }) self.module = Module(engine=self.engine, config={ "config": self.config })
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_leftclick(self, mock_input, mock_output, mock_select): def test_leftclick(self, mock_input, mock_output, mock_select):

View file

@ -17,7 +17,7 @@ class TestPulseAudioModule(unittest.TestCase):
self.config = MockConfig() self.config = MockConfig()
self.module = Module(engine=self.engine, config={ "config": self.config }) self.module = Module(engine=self.engine, config={ "config": self.config })
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_leftclick(self, mock_input, mock_output, mock_select): def test_leftclick(self, mock_input, mock_output, mock_select):
@ -26,7 +26,7 @@ class TestPulseAudioModule(unittest.TestCase):
"pactl set-source-mute @DEFAULT_SOURCE@ toggle" "pactl set-source-mute @DEFAULT_SOURCE@ toggle"
) )
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_rightclick(self, mock_input, mock_output, mock_select): def test_rightclick(self, mock_input, mock_output, mock_select):
@ -35,7 +35,7 @@ class TestPulseAudioModule(unittest.TestCase):
"pavucontrol" "pavucontrol"
) )
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_wheelup(self, mock_input, mock_output, mock_select): def test_wheelup(self, mock_input, mock_output, mock_select):
@ -44,7 +44,7 @@ class TestPulseAudioModule(unittest.TestCase):
"pactl set-source-volume @DEFAULT_SOURCE@ +2%" "pactl set-source-volume @DEFAULT_SOURCE@ +2%"
) )
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("subprocess.Popen") @mock.patch("subprocess.Popen")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_wheeldown(self, mock_input, mock_output, mock_select): def test_wheeldown(self, mock_input, mock_output, mock_select):

View file

@ -7,7 +7,7 @@ import mock
import bumblebee.input import bumblebee.input
from bumblebee.input import I3BarInput from bumblebee.input import I3BarInput
from tests.util import MockWidget, MockModule, assertPopen, assertMouseEvent from tests.util import MockWidget, MockModule, assertPopen, assertMouseEvent, MockEpoll
class TestI3BarInput(unittest.TestCase): class TestI3BarInput(unittest.TestCase):
def setUp(self): def setUp(self):
@ -21,29 +21,30 @@ class TestI3BarInput(unittest.TestCase):
def callback(self, event): def callback(self, event):
self._called += 1 self._called += 1
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_basic_read_event(self, mock_input, mock_select): def test_basic_read_event(self, mock_input, mock_select):
mock_select.return_value = (1,2,3) mock_input.readline.return_value = "somedata"
mock_input.readline.return_value = "" mock_input.fileno.return_value = 1
mock_select.return_value = MockEpoll()
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("select.epoll")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_ignore_invalid_data(self, mock_input, mock_select): def test_ignore_invalid_data(self, mock_input, mock_select):
mock_select.return_value = (1,2,3) mock_select.return_value = MockEpoll()
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("select.epoll")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_ignore_invalid_event(self, mock_input, mock_select): def test_ignore_invalid_event(self, mock_input, mock_select):
mock_select.return_value = (1,2,3) mock_select.return_value = MockEpoll()
mock_input.readline.return_value = json.dumps({ mock_input.readline.return_value = json.dumps({
"name": None, "name": None,
"instance": None, "instance": None,
@ -54,10 +55,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("select.epoll")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_ignore_partial_event(self, mock_input, mock_select): def test_ignore_partial_event(self, mock_input, mock_select):
mock_select.return_value = (1,2,3) mock_select.return_value = MockEpoll()
self.input.register_callback(None, button=1, cmd=self.callback) self.input.register_callback(None, button=1, cmd=self.callback)
mock_input.readline.return_value = json.dumps({ mock_input.readline.return_value = json.dumps({
"button": 1, "button": 1,
@ -67,7 +68,7 @@ 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("select.epoll")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_global_callback(self, mock_input, mock_select): def test_global_callback(self, mock_input, mock_select):
self.input.register_callback(None, button=1, cmd=self.callback) self.input.register_callback(None, button=1, cmd=self.callback)
@ -75,7 +76,7 @@ class TestI3BarInput(unittest.TestCase):
bumblebee.input.LEFT_MOUSE, None, "someinstance") bumblebee.input.LEFT_MOUSE, None, "someinstance")
self.assertTrue(self._called > 0) self.assertTrue(self._called > 0)
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_remove_global_callback(self, mock_input, mock_select): def test_remove_global_callback(self, mock_input, mock_select):
self.input.register_callback(None, button=1, cmd=self.callback) self.input.register_callback(None, button=1, cmd=self.callback)
@ -84,7 +85,7 @@ class TestI3BarInput(unittest.TestCase):
bumblebee.input.LEFT_MOUSE, None, "someinstance") bumblebee.input.LEFT_MOUSE, None, "someinstance")
self.assertTrue(self._called == 0) self.assertTrue(self._called == 0)
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_global_callback_button_missmatch(self, mock_input, mock_select): def test_global_callback_button_missmatch(self, mock_input, mock_select):
self.input.register_callback(self.anyModule, button=1, cmd=self.callback) self.input.register_callback(self.anyModule, button=1, cmd=self.callback)
@ -92,7 +93,7 @@ class TestI3BarInput(unittest.TestCase):
bumblebee.input.RIGHT_MOUSE, None, "someinstance") bumblebee.input.RIGHT_MOUSE, None, "someinstance")
self.assertTrue(self._called == 0) self.assertTrue(self._called == 0)
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_module_callback(self, mock_input, mock_select): def test_module_callback(self, mock_input, mock_select):
self.input.register_callback(self.anyModule, button=1, cmd=self.callback) self.input.register_callback(self.anyModule, button=1, cmd=self.callback)
@ -100,7 +101,7 @@ class TestI3BarInput(unittest.TestCase):
bumblebee.input.LEFT_MOUSE, None) bumblebee.input.LEFT_MOUSE, None)
self.assertTrue(self._called > 0) self.assertTrue(self._called > 0)
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_remove_module_callback(self, mock_input, mock_select): def test_remove_module_callback(self, mock_input, mock_select):
self.input.register_callback(self.anyModule, button=1, cmd=self.callback) self.input.register_callback(self.anyModule, button=1, cmd=self.callback)
@ -109,7 +110,7 @@ class TestI3BarInput(unittest.TestCase):
bumblebee.input.LEFT_MOUSE, None, self.anyWidget.id) bumblebee.input.LEFT_MOUSE, None, self.anyWidget.id)
self.assertTrue(self._called == 0) self.assertTrue(self._called == 0)
@mock.patch("select.select") @mock.patch("select.epoll")
@mock.patch("sys.stdin") @mock.patch("sys.stdin")
def test_widget_callback(self, mock_input, mock_select): def test_widget_callback(self, mock_input, mock_select):
self.input.register_callback(self.anyWidget, button=1, cmd=self.callback) self.input.register_callback(self.anyWidget, button=1, cmd=self.callback)
@ -117,7 +118,7 @@ class TestI3BarInput(unittest.TestCase):
bumblebee.input.LEFT_MOUSE, None, self.anyWidget.id) bumblebee.input.LEFT_MOUSE, None, self.anyWidget.id)
self.assertTrue(self._called > 0) self.assertTrue(self._called > 0)
@mock.patch("select.select") @mock.patch("select.epoll")
@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, mock_select): def test_widget_cmd_callback(self, mock_input, mock_output, mock_select):

View file

@ -23,13 +23,27 @@ def assertStateContains(test, module, state):
module.update(module.widgets()) module.update(module.widgets())
test.assertTrue(state in module.widgets()[0].state()) test.assertTrue(state in module.widgets()[0].state())
class MockEpoll(object):
def register(self, fileno, event):
pass
def poll(self, timeout):
return [(1,2)]
def unregister(self, fileno):
pass
def close(self):
pass
def assertMouseEvent(mock_input, mock_output, mock_select, engine, module, button, cmd, instance_id=None): def assertMouseEvent(mock_input, mock_output, mock_select, engine, module, button, cmd, instance_id=None):
mock_input.readline.return_value = json.dumps({ mock_input.readline.return_value = json.dumps({
"name": module.id if module else "test", "name": module.id if module else "test",
"button": button, "button": button,
"instance": instance_id "instance": instance_id
}) })
mock_select.return_value = (1, 2, 3) mock_input.fileno.return_value = 1
mock_select.return_value = MockEpoll()
engine.input.start() engine.input.start()
engine.input.stop() engine.input.stop()
mock_input.readline.assert_any_call() mock_input.readline.assert_any_call()