Skip to content

Commit cfdf99f

Browse files
Better handle startup failures - do not leave dangling processes (#201)
* Move process killing to a separate method This allows calling just the killing part of quit in a subsequent commit. This only moves some code, without changing behaviour. * Terminate process on startup errors Before, if a process was successfully started, but there would be a problem in subsequent setup (in particular the dbus connection could fail), the process could keep running in the background. This was particularly problematic when this caused the OMXPlayer constructor to raise an exception, since then no player instance would be created so the process pid would be lost. This commit ensures that whenever the OMXPlayer constructor or load method throws, no process will be dangling in the background. This fixes #196 * Add more tests for player.quit This adds test coverage for two exception handling cases.
1 parent b1a11bf commit cfdf99f

File tree

2 files changed

+70
-16
lines changed

2 files changed

+70
-16
lines changed

omxplayer/player.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,15 @@ def monitor(self, process, on_exit):
190190
stdin=devnull,
191191
stdout=devnull,
192192
preexec_fn=os.setsid)
193-
self._process_monitor = threading.Thread(target=monitor,
194-
args=(self, process, on_exit))
195-
self._process_monitor.start()
196-
return process
193+
try:
194+
self._process_monitor = threading.Thread(target=monitor,
195+
args=(self, process, on_exit))
196+
self._process_monitor.start()
197+
return process
198+
except:
199+
# Make sure to not leave any dangling process on failure
200+
self._terminate_process(process)
201+
raise
197202

198203
def _setup_omxplayer_process(self, source):
199204
logger.debug('Setting up OMXPlayer process')
@@ -205,6 +210,14 @@ def _setup_omxplayer_process(self, source):
205210
atexit.register(self.quit)
206211
return process
207212

213+
def _terminate_process(self, process):
214+
try:
215+
process_group_id = os.getpgid(process.pid)
216+
os.killpg(process_group_id, signal.SIGTERM)
217+
logger.debug('SIGTERM Sent to pid: %s' % process_group_id)
218+
except OSError:
219+
logger.error('Could not find the process to kill')
220+
208221
def _setup_dbus_connection(self, Connection, bus_address_finder):
209222
logger.debug('Trying to connect to OMXPlayer via DBus')
210223
tries = 0
@@ -234,10 +247,17 @@ def load(self, source, pause=False):
234247
source (string): Path to the file to play or URL
235248
"""
236249
self._source = source
237-
self._load_source(source)
238-
if pause:
239-
time.sleep(0.5) # Wait for the DBus interface to be initialised
240-
self.pause()
250+
try:
251+
self._load_source(source)
252+
if pause:
253+
time.sleep(0.5) # Wait for the DBus interface to be initialised
254+
self.pause()
255+
except:
256+
# Make sure we do not leave any dangling process
257+
if self._process:
258+
self._terminate_process(self._process)
259+
self._process = None
260+
raise
241261

242262
""" ROOT INTERFACE PROPERTIES """
243263

@@ -846,15 +866,10 @@ def quit(self):
846866
if self._process is None:
847867
logger.debug('Quit was called after self._process had already been released')
848868
return
849-
try:
850-
logger.debug('Quitting OMXPlayer')
851-
process_group_id = os.getpgid(self._process.pid)
852-
os.killpg(process_group_id, signal.SIGTERM)
853-
logger.debug('SIGTERM Sent to pid: %s' % process_group_id)
854-
self._process_monitor.join()
855-
except OSError:
856-
logger.error('Could not find the process to kill')
857869

870+
logger.debug('Quitting OMXPlayer')
871+
self._terminate_process(self._process)
872+
self._process_monitor.join()
858873
self._process = None
859874

860875
@_check_player_is_active

tests/unit/test_omxplayer.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@ def test_tries_to_open_dbus_again_if_it_cant_connect(self, *args):
4646
self.patch_and_run_omxplayer(Connection=dbus_connection)
4747
self.assertEqual(50, self.player.tries)
4848

49+
def test_dbus_failure_kills(self, popen, sleep, isfile, killpg, *args):
50+
omxplayer_process = Mock()
51+
popen.return_value = omxplayer_process
52+
dbus_connection = Mock(side_effect=DBusConnectionError)
53+
with patch('os.getpgid', Mock(return_value=omxplayer_process.pid)):
54+
with self.assertRaises(SystemError):
55+
self.patch_and_run_omxplayer(Connection=dbus_connection)
56+
killpg.assert_called_once_with(omxplayer_process.pid, signal.SIGTERM)
57+
58+
def test_thread_failure_kills(self, popen, sleep, isfile, killpg, *args):
59+
omxplayer_process = Mock()
60+
popen.return_value = omxplayer_process
61+
with patch ('threading.Thread', Mock(side_effect=RuntimeError)):
62+
with patch('os.getpgid', Mock(return_value=omxplayer_process.pid)):
63+
with self.assertRaises(RuntimeError):
64+
self.patch_and_run_omxplayer()
65+
killpg.assert_called_once_with(omxplayer_process.pid, signal.SIGTERM)
66+
4967
@parameterized.expand([
5068
['can_quit', 'CanQuit', [], []],
5169
['can_set_fullscreen', 'CanSetFullscreen', [], []],
@@ -128,6 +146,27 @@ def test_quitting_waits_for_omxplayer_to_die(self, popen, *args):
128146
self.player.quit()
129147
omxplayer_process.wait.assert_has_calls([call()])
130148

149+
def test_quitting_when_already_dead(self, popen, sleep, isfile, killpg, *args):
150+
omxplayer_process = Mock()
151+
popen.return_value = omxplayer_process
152+
self.patch_and_run_omxplayer()
153+
# Pretend the process is already dead
154+
killpg.configure_mock(side_effect=OSError)
155+
with patch('os.getpgid', Mock(return_value=omxplayer_process.pid)):
156+
# This tests that quit handles the OSError
157+
self.player.quit()
158+
killpg.assert_called_once_with(omxplayer_process.pid, signal.SIGTERM)
159+
160+
def test_quitting_twice(self, popen, sleep, isfile, killpg, *args):
161+
omxplayer_process = Mock()
162+
popen.return_value = omxplayer_process
163+
self.patch_and_run_omxplayer()
164+
with patch('os.getpgid', Mock(return_value=omxplayer_process.pid)):
165+
# This should not raise, and call killpg once
166+
self.player.quit()
167+
self.player.quit()
168+
killpg.assert_called_once_with(omxplayer_process.pid, signal.SIGTERM)
169+
131170
def test_check_process_still_exists_before_dbus_call(self, *args):
132171
self.patch_and_run_omxplayer()
133172
self.player._process = process = Mock(return_value=None)

0 commit comments

Comments
 (0)