Fix hanging on shell-pipe
Properly closes standard I/O streams to prevent shell-piped processes from hanging infinitely on a dead pipe. Change-Id: Id9dedac4f778cf37d626f2777d519f73cc4b5f2c Story: 2007219 Task: 38471
This commit is contained in:
parent
834d02e616
commit
2651f13f28
@ -0,0 +1,5 @@
|
||||
---
|
||||
fixes:
|
||||
- |
|
||||
Properly closes standard I/O streams to prevent shell-piped processes from
|
||||
hanging infinitely on a dead pipe.
|
@ -12,7 +12,6 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
@ -28,6 +27,7 @@ from virtualbmc.cmd import vbmcd
|
||||
from virtualbmc import config as vbmc_config
|
||||
from virtualbmc.exception import VirtualBMCError
|
||||
from virtualbmc import log
|
||||
from virtualbmc import utils
|
||||
|
||||
CONF = vbmc_config.get_config()
|
||||
|
||||
@ -110,9 +110,10 @@ class ZmqClient(object):
|
||||
"future releases!")
|
||||
|
||||
# attempt to start and daemonize the server
|
||||
if os.fork() == 0:
|
||||
# this will also fork and detach properly
|
||||
vbmcd.main([])
|
||||
with utils.detach_process() as pid:
|
||||
if pid == 0:
|
||||
# NOTE(etingof): this child will never return
|
||||
vbmcd.main(['--foreground'])
|
||||
|
||||
# TODO(etingof): perform some more retries
|
||||
time.sleep(CONF['default']['server_spawn_wait'] / 1000.)
|
||||
|
@ -85,9 +85,14 @@ def main(argv=sys.argv[1:]):
|
||||
|
||||
if args.foreground:
|
||||
return wrap_with_pidfile(control.application, os.getpid())
|
||||
|
||||
else:
|
||||
with utils.detach_process() as pid:
|
||||
if pid > 0:
|
||||
return 0
|
||||
|
||||
return wrap_with_pidfile(control.application, pid)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
@ -30,22 +30,23 @@ class VBMCDTestCase(base.TestCase):
|
||||
@mock.patch.object(os, 'kill')
|
||||
@mock.patch.object(os, 'unlink')
|
||||
def test_main_foreground(self, mock_unlink, mock_kill, mock_open):
|
||||
with mock.patch.object(control, 'application') as ml:
|
||||
with mock.patch.object(control, 'application') as mock_ml:
|
||||
mock_kill.side_effect = OSError()
|
||||
vbmcd.main(['--foreground'])
|
||||
mock_kill.assert_called_once()
|
||||
ml.assert_called_once()
|
||||
mock_ml.assert_called_once()
|
||||
mock_unlink.assert_called_once()
|
||||
|
||||
@mock.patch.object(builtins, 'open')
|
||||
@mock.patch.object(os, 'kill')
|
||||
@mock.patch.object(os, 'unlink')
|
||||
def test_main_background(self, mock_unlink, mock_kill, mock_open):
|
||||
with mock.patch.object(utils, 'detach_process') as dp, \
|
||||
mock.patch.object(control, 'application') as ml:
|
||||
mock_kill.side_effect = OSError()
|
||||
vbmcd.main([])
|
||||
mock_kill.assert_called_once()
|
||||
dp.assert_called_once()
|
||||
ml.assert_called_once()
|
||||
mock_unlink.assert_called_once()
|
||||
with mock.patch.object(utils, 'detach_process') as mock_dp:
|
||||
with mock.patch.object(control, 'application') as mock_ml:
|
||||
mock_kill.side_effect = OSError()
|
||||
mock_dp.return_value.__enter__.return_value = 0
|
||||
vbmcd.main([])
|
||||
mock_kill.assert_called_once()
|
||||
mock_dp.assert_called_once()
|
||||
mock_ml.assert_called_once()
|
||||
mock_unlink.assert_called_once()
|
||||
|
@ -114,60 +114,49 @@ class LibvirtUtilsTestCase(base.TestCase):
|
||||
self._test_libvirt_open_sasl(readonly=True)
|
||||
|
||||
|
||||
@mock.patch.object(os, 'getpid')
|
||||
@mock.patch.object(os, 'umask')
|
||||
@mock.patch.object(os, 'chdir')
|
||||
@mock.patch.object(os, 'setsid')
|
||||
@mock.patch.object(os, '_exit')
|
||||
@mock.patch.object(os, 'fork')
|
||||
@mock.patch.object(utils, 'os')
|
||||
class DetachProcessUtilsTestCase(base.TestCase):
|
||||
|
||||
def test_detach_process(self, mock_fork, mock__exit, mock_setsid,
|
||||
mock_chdir, mock_umask, mock_getpid):
|
||||
def test_detach_process(self, mock_os):
|
||||
|
||||
# 2nd value > 0 so _exit get called and we can assert that we've
|
||||
# killed the parent's process
|
||||
mock_fork.side_effect = (0, 999)
|
||||
mock_os.fork.side_effect = (0, 999)
|
||||
mock_os.devnull = os.devnull
|
||||
|
||||
mock_getpid.return_value = 123
|
||||
with utils.detach_process() as pid:
|
||||
self.assertEqual(123, pid)
|
||||
self.assertEqual(0, pid)
|
||||
|
||||
# assert fork() has been called twice
|
||||
expected_fork_calls = [mock.call()] * 2
|
||||
self.assertEqual(expected_fork_calls, mock_fork.call_args_list)
|
||||
self.assertEqual(expected_fork_calls, mock_os.fork.call_args_list)
|
||||
|
||||
mock_setsid.assert_called_once_with()
|
||||
mock_chdir.assert_called_once_with('/')
|
||||
mock_umask.assert_called_once_with(0)
|
||||
mock__exit.assert_called_once_with(0)
|
||||
mock_getpid.assert_called_once_with()
|
||||
mock_os.setsid.assert_called_once_with()
|
||||
mock_os.chdir.assert_called_once_with('/')
|
||||
mock_os.umask.assert_called_once_with(0)
|
||||
mock_os._exit.assert_called_once_with(0)
|
||||
|
||||
def test_detach_process_fork_fail(self, mock_fork, mock__exit, mock_setsid,
|
||||
mock_chdir, mock_umask, mock_getpid):
|
||||
def test_detach_process_fork_fail(self, mock_os):
|
||||
error_msg = 'Kare-a-tay!'
|
||||
mock_fork.side_effect = OSError(error_msg)
|
||||
mock_os.fork.side_effect = OSError(error_msg)
|
||||
|
||||
with self.assertRaisesRegex(exception.DetachProcessError, error_msg):
|
||||
with utils.detach_process():
|
||||
pass
|
||||
|
||||
mock_fork.assert_called_once_with()
|
||||
self.assertFalse(mock_setsid.called)
|
||||
self.assertFalse(mock_chdir.called)
|
||||
self.assertFalse(mock_umask.called)
|
||||
self.assertFalse(mock__exit.called)
|
||||
self.assertFalse(mock_getpid.called)
|
||||
mock_os.fork.assert_called_once_with()
|
||||
self.assertFalse(mock_os.setsid.called)
|
||||
self.assertFalse(mock_os.chdir.called)
|
||||
self.assertFalse(mock_os.umask.called)
|
||||
self.assertFalse(mock_os._exit.called)
|
||||
|
||||
def test_detach_process_chdir_fail(self, mock_fork, mock__exit,
|
||||
mock_setsid, mock_chdir, mock_umask,
|
||||
mock_getpid):
|
||||
def test_detach_process_chdir_fail(self, mock_os):
|
||||
# 2nd value > 0 so _exit get called and we can assert that we've
|
||||
# killed the parent's process
|
||||
mock_fork.side_effect = (0, 999)
|
||||
mock_os.fork.side_effect = (0, 999)
|
||||
|
||||
error_msg = 'Fish paste!'
|
||||
mock_chdir.side_effect = Exception(error_msg)
|
||||
mock_os.chdir.side_effect = Exception(error_msg)
|
||||
|
||||
with self.assertRaisesRegex(exception.DetachProcessError, error_msg):
|
||||
with utils.detach_process():
|
||||
@ -175,23 +164,20 @@ class DetachProcessUtilsTestCase(base.TestCase):
|
||||
|
||||
# assert fork() has been called twice
|
||||
expected_fork_calls = [mock.call()] * 2
|
||||
self.assertEqual(expected_fork_calls, mock_fork.call_args_list)
|
||||
self.assertEqual(expected_fork_calls, mock_os.fork.call_args_list)
|
||||
|
||||
mock_setsid.assert_called_once_with()
|
||||
mock_chdir.assert_called_once_with('/')
|
||||
mock__exit.assert_called_once_with(0)
|
||||
self.assertFalse(mock_umask.called)
|
||||
self.assertFalse(mock_getpid.called)
|
||||
mock_os.setsid.assert_called_once_with()
|
||||
mock_os.chdir.assert_called_once_with('/')
|
||||
mock_os._exit.assert_called_once_with(0)
|
||||
self.assertFalse(mock_os.umask.called)
|
||||
|
||||
def test_detach_process_umask_fail(self, mock_fork, mock__exit,
|
||||
mock_setsid, mock_chdir, mock_umask,
|
||||
mock_getpid):
|
||||
def test_detach_process_umask_fail(self, mock_os):
|
||||
# 2nd value > 0 so _exit get called and we can assert that we've
|
||||
# killed the parent's process
|
||||
mock_fork.side_effect = (0, 999)
|
||||
mock_os.fork.side_effect = (0, 999)
|
||||
|
||||
error_msg = 'Barnacles!'
|
||||
mock_umask.side_effect = Exception(error_msg)
|
||||
mock_os.umask.side_effect = Exception(error_msg)
|
||||
|
||||
with self.assertRaisesRegex(exception.DetachProcessError, error_msg):
|
||||
with utils.detach_process():
|
||||
@ -199,10 +185,9 @@ class DetachProcessUtilsTestCase(base.TestCase):
|
||||
|
||||
# assert fork() has been called twice
|
||||
expected_fork_calls = [mock.call()] * 2
|
||||
self.assertEqual(expected_fork_calls, mock_fork.call_args_list)
|
||||
self.assertEqual(expected_fork_calls, mock_os.fork.call_args_list)
|
||||
|
||||
mock_setsid.assert_called_once_with()
|
||||
mock_chdir.assert_called_once_with('/')
|
||||
mock__exit.assert_called_once_with(0)
|
||||
mock_umask.assert_called_once_with(0)
|
||||
self.assertFalse(mock_getpid.called)
|
||||
mock_os.setsid.assert_called_once_with()
|
||||
mock_os.chdir.assert_called_once_with('/')
|
||||
mock_os._exit.assert_called_once_with(0)
|
||||
mock_os.umask.assert_called_once_with(0)
|
||||
|
@ -10,8 +10,10 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import libvirt
|
||||
import os
|
||||
import sys
|
||||
|
||||
import libvirt
|
||||
|
||||
from virtualbmc import exception
|
||||
|
||||
@ -97,12 +99,14 @@ def mask_dict_password(dictionary, secret='***'):
|
||||
class detach_process(object):
|
||||
"""Detach the process from its parent and session."""
|
||||
|
||||
def _fork(self):
|
||||
def _fork(self, parent_exits):
|
||||
try:
|
||||
ret = os.fork()
|
||||
if ret > 0:
|
||||
# Exit the parent process
|
||||
pid = os.fork()
|
||||
if pid > 0 and parent_exits:
|
||||
os._exit(0)
|
||||
|
||||
return pid
|
||||
|
||||
except OSError as e:
|
||||
raise exception.DetachProcessError(error=e)
|
||||
|
||||
@ -133,13 +137,29 @@ class detach_process(object):
|
||||
raise exception.DetachProcessError(error=error)
|
||||
|
||||
def __enter__(self):
|
||||
self._fork()
|
||||
pid = self._fork(parent_exits=False)
|
||||
if pid > 0:
|
||||
return pid
|
||||
|
||||
os.setsid()
|
||||
self._fork()
|
||||
|
||||
self._fork(parent_exits=True)
|
||||
|
||||
self._change_root_directory()
|
||||
self._change_file_creation_mask()
|
||||
|
||||
return os.getpid()
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
si = open(os.devnull, 'r')
|
||||
so = open(os.devnull, 'a+')
|
||||
se = open(os.devnull, 'a+')
|
||||
|
||||
os.dup2(si.fileno(), sys.stdin.fileno())
|
||||
os.dup2(so.fileno(), sys.stdout.fileno())
|
||||
os.dup2(se.fileno(), sys.stderr.fileno())
|
||||
|
||||
return pid
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
pass
|
||||
|
Loading…
Reference in New Issue
Block a user