Merge lp:~roadmr/checkbox/588539 into lp:checkbox

Proposed by Daniel Manrique
Status: Merged
Merged at revision: 918
Proposed branch: lp:~roadmr/checkbox/588539
Merge into: lp:checkbox
Diff against target: 433 lines (+322/-8)
6 files modified
backend (+2/-0)
checkbox/lib/enum.py (+72/-0)
checkbox/lib/fifo.py (+29/-4)
checkbox/lib/selector.py (+207/-0)
debian/changelog (+4/-1)
plugins/backend_info.py (+8/-3)
To merge this branch: bzr merge lp:~roadmr/checkbox/588539
Reviewer Review Type Date Requested Status
Marc Tardif (community) Approve
Review via email: mp+63428@code.launchpad.net

This proposal supersedes a proposal from 2011-06-03.

Description of the change

Modified frontend/backend interaction and FIFO handling so that:

1) The frontend doesn't hang if the backend fails to start
2) The frontend returns "empty" if the frontend hasn't responded to a message in a while
3) The frontend explicitly asks the backend to end when the test run finishes.

This takes care directly of bug 588539. By tweaking the interactive behavior when the backend fails to respond, it should also lead to an eventual fix for bug 334893.

(Rev. 921 - Fixed according to Marc's comments, and refactored some poorly-written (by me) code).

To post a comment you must log in.
Revision history for this message
Marc Tardif (cr3) wrote : Posted in a previous version of this proposal

First, you might like to use the default parameter when declaring the timeout property in the BackendInfo class rather than create an additional entry in examples/checkbox.ini. Also, I think you want to declare it as an Int rather than a String property variable.

Second, you forgot a pdb statement in the stop message handler.

review: Needs Fixing
lp:~roadmr/checkbox/588539 updated
922. By Daniel Manrique

removing stray and unneeded config entry

Revision history for this message
Marc Tardif (cr3) wrote :

Looks good, merging and building package.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'backend'
2--- backend 2010-04-20 16:23:16 +0000
3+++ backend 2011-06-03 20:51:05 +0000
4@@ -28,6 +28,8 @@
5 while True:
6 try:
7 message = reader.read_object()
8+ if message == "stop":
9+ break
10 if isinstance(message, dict) and "command" in message:
11 job = Job(message["command"], message.get("environ"),
12 message.get("timeout"))
13
14=== added file 'checkbox/lib/enum.py'
15--- checkbox/lib/enum.py 1970-01-01 00:00:00 +0000
16+++ checkbox/lib/enum.py 2011-06-03 20:51:05 +0000
17@@ -0,0 +1,72 @@
18+#
19+# This file is part of Checkbox.
20+#
21+# Copyright 2011 Canonical Ltd.
22+#
23+# Checkbox is free software: you can redistribute it and/or modify
24+# it under the terms of the GNU General Public License as published by
25+# the Free Software Foundation, either version 3 of the License, or
26+# (at your option) any later version.
27+#
28+# Checkbox is distributed in the hope that it will be useful,
29+# but WITHOUT ANY WARRANTY; without even the implied warranty of
30+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31+# GNU General Public License for more details.
32+#
33+# You should have received a copy of the GNU General Public License
34+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
35+#
36+__all__ = [
37+ "EnumException", "Enum", "enum_name_to_value", "enum_value_to_name"]
38+
39+
40+class EnumException(Exception):
41+
42+ pass
43+
44+
45+class Enum(object):
46+
47+ def __init__(self, *names):
48+ value = 0
49+ unique_names = set()
50+ for name in names:
51+ if isinstance(name, (list, tuple)):
52+ name, value = name
53+
54+ if not isinstance(name, str):
55+ raise EnumException(
56+ "enum name is not a string: %s" % name)
57+
58+ if isinstance(value, str):
59+ if value not in unique_names:
60+ raise EnumException(
61+ "enum value does not define: %s" % value)
62+ value = getattr(self, value)
63+
64+ if not isinstance(value, (int, long)):
65+ raise EnumException(
66+ "enum value is not an integer: %s" % value)
67+
68+ if name in unique_names:
69+ raise EnumException(
70+ "enum name is not unique: %s" % name)
71+
72+ unique_names.add(name)
73+ setattr(self, name, value)
74+ value += 1
75+
76+
77+def enum_name_to_value(enum, name):
78+ if not hasattr(enum, name):
79+ raise EnumException("enum name unknown: %s" % name)
80+
81+ return getattr(enum, name)
82+
83+
84+def enum_value_to_name(enum, value):
85+ for k, v in enum.__dict__.items():
86+ if v == value:
87+ return k
88+
89+ raise EnumException("enum value unknown: %s" % value)
90
91=== modified file 'checkbox/lib/fifo.py'
92--- checkbox/lib/fifo.py 2010-03-28 03:00:14 +0000
93+++ checkbox/lib/fifo.py 2011-06-03 20:51:05 +0000
94@@ -20,15 +20,16 @@
95 import struct
96
97 from checkbox.contrib.bpickle import dumps, loads
98-
99+from checkbox.lib.selector import Selector, SelectorIO
100
101 class FifoBase(object):
102
103 mode = None
104
105- def __init__(self, path):
106+ def __init__(self, path, timeout=None):
107 self.path = path
108 self.file = open(path, self.mode)
109+ self._timeout = timeout
110
111 def __del__(self):
112 self.close()
113@@ -36,12 +37,29 @@
114 def close(self):
115 self.file.close()
116
117+ def wait_for(self, operation):
118+ if self._timeout is not None:
119+ selector = Selector()
120+ selector.set_timeout(self._timeout)
121+ selector.add_fd(self.file.fileno(), operation)
122+
123+ selector.execute()
124+
125+ if not selector.has_ready():
126+ return False
127+ return True
128
129 class FifoReader(FifoBase):
130
131- mode = "r"
132+ #on Linux, opening a FIFO in read-write mode is non-blocking and
133+ #succeeds even if other end is not open as per FIFO(7)
134+ mode = "w+"
135
136 def read_string(self):
137+ # Check if a connection arrived within the timeout
138+ if not self.wait_for(SelectorIO.READ):
139+ return None
140+
141 size = struct.calcsize("i")
142 length_string = self.file.read(size)
143 if not length_string:
144@@ -60,9 +78,16 @@
145
146 class FifoWriter(FifoBase):
147
148- mode = "w"
149+ #on Linux, opening a FIFO in read-write mode is non-blocking and
150+ #succeeds even if other end is not open as per FIFO(7)
151+ mode = "w+"
152
153 def write_string(self, string):
154+
155+ # Wait until I can write
156+ if not self.wait_for(SelectorIO.WRITE):
157+ return None
158+
159 length = len(string)
160 length_string = struct.pack(">i", length)
161 self.file.write(length_string)
162
163=== added file 'checkbox/lib/selector.py'
164--- checkbox/lib/selector.py 1970-01-01 00:00:00 +0000
165+++ checkbox/lib/selector.py 2011-06-03 20:51:05 +0000
166@@ -0,0 +1,207 @@
167+#
168+# This file is part of Checkbox.
169+#
170+# Copyright 2011 Canonical Ltd.
171+#
172+# Checkbox is free software: you can redistribute it and/or modify
173+# it under the terms of the GNU General Public License as published by
174+# the Free Software Foundation, either version 3 of the License, or
175+# (at your option) any later version.
176+#
177+# Checkbox is distributed in the hope that it will be useful,
178+# but WITHOUT ANY WARRANTY; without even the implied warranty of
179+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
180+# GNU General Public License for more details.
181+#
182+# You should have received a copy of the GNU General Public License
183+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
184+#
185+import os
186+import errno
187+import select
188+
189+from checkbox.lib.enum import Enum
190+
191+
192+__all__ = ["SelectorIO", "SelectorState", "Selector"]
193+
194+
195+SelectorIO = Enum(
196+ "READ",
197+ "WRITE",
198+ "EXCEPT")
199+
200+SelectorState = Enum(
201+ "VIRGIN",
202+ "READY",
203+ "TIMED_OUT",
204+ "SIGNALED",
205+ "FAILED")
206+
207+
208+class Selector(object):
209+ __slots__ = ("_read_fds", "_write_fds", "_except_fds",
210+ "_save_read_fds", "_save_write_fds", "_save_except_fds",
211+ "_fd_set_size", "_timeout", "_state", "_errno")
212+
213+ _fd_select_size = -1
214+ _cached_read_fds = None
215+ _cached_save_read_fds = None
216+ _cached_write_fds = None
217+ _cached_save_write_fds = None
218+ _cached_except_fds = None
219+ _cached_save_except_fds = None
220+
221+ def __init__(self):
222+ if Selector._cached_read_fds:
223+ self._read_fds = Selector._cached_read_fds
224+ self._write_fds = Selector._cached_write_fds
225+ self._except_fds = Selector._cached_except_fds
226+
227+ self._save_read_fds = Selector._cached_save_read_fds
228+ self._save_write_fds = Selector._cached_save_write_fds
229+ self._save_except_fds = Selector._cached_save_except_fds
230+
231+ Selector._cached_read_fds = None
232+ Selector._cached_write_fds = None
233+ Selector._cached_except_fds = None
234+
235+ Selector._cached_save_read_fds = None
236+ Selector._cached_save_write_fds = None
237+ Selector._cached_save_except_fds = None
238+
239+ else:
240+ self._read_fds = []
241+ self._write_fds = []
242+ self._except_fds = []
243+
244+ self._save_read_fds = []
245+ self._save_write_fds = []
246+ self._save_except_fds = []
247+
248+ self.reset()
249+
250+ def __del__(self):
251+ if Selector._cached_read_fds is None:
252+ Selector._cached_read_fds = self._read_fds
253+ Selector._cached_write_fds = self._write_fds
254+ Selector._cached_except_fds = self._except_fds
255+
256+ Selector._cached_save_read_fds = self._save_read_fds
257+ Selector._cached_save_write_fds = self._save_write_fds
258+ Selector._cached_save_except_fds = self._save_except_fds
259+
260+ def reset(self):
261+ self._errno = 0
262+ self._state = SelectorState.VIRGIN
263+ self._timeout = None
264+ self._save_read_fds = []
265+ self._save_write_fds = []
266+ self._save_except_fds = []
267+
268+ @staticmethod
269+ def get_fd_select_size():
270+ if Selector._fd_select_size == -1:
271+ try:
272+ Selector._fd_select_size = os.sysconf("SC_OPEN_MAX")
273+ except (AttributeError, ValueError):
274+ Selector._fd_select_size = 1024
275+
276+ return Selector._fd_select_size
277+
278+ def get_errno(self):
279+ return self._errno
280+
281+ def get_errstr(self):
282+ return os.strerror(self._errno)
283+
284+ def set_timeout(self, timeout):
285+ self._timeout = timeout
286+
287+ def unset_timeout(self):
288+ self._timeout = None
289+
290+ def has_timed_out(self):
291+ return self._state == SelectorState.TIMED_OUT
292+
293+ def has_signaled(self):
294+ return self._state == SelectorState.SIGNALED
295+
296+ def has_failed(self):
297+ return self._state == SelectorState.FAILED
298+
299+ def has_ready(self):
300+ return self._state == SelectorState.READY
301+
302+ def add_fd(self, fd, interest):
303+ if fd < 0 or fd >= Selector.get_fd_select_size():
304+ raise Exception("File descriptor %d outside of range 0-%d"
305+ % (fd, Selector._fd_select_size))
306+
307+ if interest == SelectorIO.READ:
308+ self._save_read_fds.append(fd)
309+
310+ elif interest == SelectorIO.WRITE:
311+ self._save_write_fds.append(fd)
312+
313+ elif interest == SelectorIO.EXCEPT:
314+ self._save_except_fds.append(fd)
315+
316+ def remove_fd(self, fd, interest):
317+ if fd < 0 or fd >= Selector.get_fd_select_size():
318+ raise Exception("File descriptor %d outside of range 0-%d"
319+ % (fd, Selector._fd_select_size))
320+
321+ if interest == SelectorIO.READ:
322+ self._save_read_fds.remove(fd)
323+
324+ elif interest == SelectorIO.WRITE:
325+ self._save_write_fds.remove(fd)
326+
327+ elif interest == SelectorIO.EXCEPT:
328+ self._save_except_fds.remove(fd)
329+
330+ def execute(self):
331+ try:
332+ self._read_fds, self._write_fds, self._except_fds = select.select(
333+ self._save_read_fds, self._save_write_fds,
334+ self._save_except_fds, self._timeout)
335+ except select.error, e:
336+ self._errno = e.errno
337+ if e.errno == errno.EINTR:
338+ self._state = SelectorState.SIGNALED
339+
340+ else:
341+ self._state = SelectorState.FAILED
342+
343+ return
344+
345+ # Just in case
346+ self._errno = 0
347+ if not self._read_fds \
348+ and not self._write_fds \
349+ and not self._except_fds:
350+ self._state = SelectorState.TIMED_OUT
351+
352+ else:
353+ self._state = SelectorState.READY
354+
355+ def is_fd_ready(self, fd, interest):
356+ if self._state != SelectorState.READY \
357+ and self._state != SelectorState.TIMED_OUT:
358+ raise Exception(
359+ "Selector requested descriptor not in ready state")
360+
361+ if fd < 0 or fd >= Selector.get_fd_select_size():
362+ return False
363+
364+ if interest == SelectorIO.READ:
365+ return fd in self._read_fds
366+
367+ elif interest == SelectorIO.WRITE:
368+ return fd in self._write_fds
369+
370+ elif interest == SelectorIO.EXCEPT:
371+ return fd in self._except_fds
372+
373+ return False
374
375=== modified file 'debian/changelog'
376--- debian/changelog 2011-05-25 14:10:15 +0000
377+++ debian/changelog 2011-06-03 20:51:05 +0000
378@@ -3,11 +3,14 @@
379 [Daniel Manrique]
380 * Fix GUI definition file so main window uses "natural request", growing
381 when child widgets require so (LP: #776734)
382+ * Fix open/read blocking behavior and backend/frontend communications to
383+ avoid hangs and lingering backends. (LP: #588539)
384
385 [Robert Roth]
386 * Improve command line key prompts (LP: #786924)
387
388- -- Marc Tardif <marc@ubuntu.com> Wed, 25 May 2011 10:09:22 -0400
389+
390+ -- Daniel Manrique <daniel.manrique@canonical.com> Fri, 03 Jun 2011 15:23:05 -0400
391
392 checkbox (0.12) oneiric; urgency=low
393
394
395=== modified file 'plugins/backend_info.py'
396--- plugins/backend_info.py 2011-05-24 19:15:37 +0000
397+++ plugins/backend_info.py 2011-06-03 20:51:05 +0000
398@@ -25,11 +25,15 @@
399 from checkbox.lib.fifo import FifoReader, FifoWriter, create_fifo
400
401 from checkbox.plugin import Plugin
402-from checkbox.properties import Path
403+from checkbox.properties import Path, Float
404
405
406 class BackendInfo(Plugin):
407
408+ # how long to wait for I/O from/to the backend. If it takes longer
409+ # than this, the message is ignored.
410+ timeout = Float(default=15.0)
411+
412 command = Path(default="%(checkbox_share)s/backend")
413
414 def register(self, manager):
415@@ -76,8 +80,8 @@
416
417 self.pid = os.fork()
418 if self.pid > 0:
419- self.parent_writer = FifoWriter(child_input)
420- self.parent_reader = FifoReader(child_output)
421+ self.parent_writer = FifoWriter(child_input, timeout=self.timeout)
422+ self.parent_reader = FifoReader(child_output, timeout=self.timeout)
423
424 else:
425 root_command = self.get_root_command(child_input, child_output)
426@@ -92,6 +96,7 @@
427 self._manager.reactor.fire("message-result", *result)
428
429 def stop(self):
430+ self.parent_writer.write_object("stop")
431 self.parent_writer.close()
432 self.parent_reader.close()
433 shutil.rmtree(self.directory)

Subscribers

People subscribed via source and target branches