Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Disable forking when xdebug is on

Summary:
Forking-on-every-command seems to cause dbgp-phpsh to infinite loop and
consume cpu. Make the forking enabled only when -X flag is set, i.e.
when xdebug is disabled.
  • Loading branch information...
commit 4baee253bdb5f3c81150e60d166840567c7a1d12 1 parent 09ded0c
Yiding Jia authored adonohue committed
Showing with 131 additions and 17 deletions.
  1. +18 −5 src/__init__.py
  2. +5 −3 src/phpsh
  3. +41 −9 src/phpsh.php
  4. +67 −0 src/tests.py
View
23 src/__init__.py
@@ -426,6 +426,8 @@ def __init__(self, cmd_incs, do_color, do_echo, codebase_mode,
self.comm_base += " -A"
if self.config.get_option("General", "UndefinedFunctionCheck") == "no":
self.comm_base += " -u"
+ if not self.with_xdebug:
+ self.comm_base += " -f"
self.cmd_incs = cmd_incs
# ctags integration
@@ -726,6 +728,7 @@ def wait_for_comm_finish(self, defer_output=False):
if ret_code != None:
if debug:
print "NOOOOO"
+ print "subprocess died with return code: " + repr(ret_code)
died = True
break
while not died:
@@ -747,7 +750,6 @@ def wait_for_comm_finish(self, defer_output=False):
out_buff_i = 1
buff = os.read(r.fileno(), buffer_size)
if not buff:
- # process has died
died = True
break
out_buff[out_buff_i] += buff
@@ -763,9 +765,15 @@ def wait_for_comm_finish(self, defer_output=False):
err.write(l)
out_buff[out_buff_i] = \
out_buff[out_buff_i][last_nl_pos + 1:]
- # don't sleep if the command is already done
- # (even tho sleep period is small; maximize responsiveness)
- if self.comm_file.readline():
+ # at this point either:
+ # the php instance died
+ # select timed out
+ l = self.comm_file.readline()
+ if l.startswith("child"):
+ ret_code = self.p.poll()
+ os.kill(self.p.pid, signal.SIGHUP)
+ self.p.pid = int(l.split()[1])
+ elif l.startswith("ready"):
break
time.sleep(comm_poll_timeout)
@@ -1001,6 +1009,8 @@ def end_process(self, alarm=False):
# shutdown php, if it doesn't exit in 5s, kill -9
if alarm:
signal.signal(signal.SIGALRM, sigalrm_handler)
+ # if we have fatal-restart prevention, the child proess can't be waited
+ # on since it's no longer a child of this process
try:
self.p.stdout.close()
self.p.stderr.close()
@@ -1011,6 +1021,9 @@ def end_process(self, alarm=False):
except (IOError, OSError, KeyboardInterrupt):
os.kill(self.p.pid, signal.SIGKILL)
# collect the zombie
- os.waitpid(self.p.pid, 0)
+ try:
+ os.waitpid(self.p.pid, 0)
+ except (OSError):
+ pass
self.p = None
View
8 src/phpsh
@@ -23,8 +23,10 @@ p.add_option("-A", "--no-autocomplete", action="store_true")
p.add_option("-C", "--no-color", action="store_true")
p.add_option("-M", "--no-multiline", action="store_true")
p.add_option("-T", "--no-ctags", action="store_true")
-p.add_option("-X", "--no-xdebug", action="store_true",
- help="Disable PHP debugging with xdebug.")
+
+p.add_option("-X", "--xdebug", action="store_true",
+ help="Enable PHP debugging with xdebug (this also disable" +
+ " forking to save state on fatals)")
(opts, cmd_incs) = p.parse_args()
# default codebase_mode is "" (don't want None)
@@ -37,7 +39,7 @@ s = PhpshState(cmd_incs=set(cmd_incs),
do_color=not opts.no_color and not opts.test_file,
do_echo=not opts.test_file, codebase_mode=opts.codebase_mode,
do_autocomplete=not opts.no_autocomplete, do_ctags=not opts.no_ctags,
- interactive=not opts.test_file, with_xdebug=not opts.no_xdebug,
+ interactive=not opts.test_file, with_xdebug=opts.xdebug,
verbose=opts.verbose)
if opts.test_file:
View
50 src/phpsh.php
@@ -38,6 +38,7 @@
$___phpsh___do_autocomplete = true;
$___phpsh___do_undefined_function_check = true;
$___phpsh___options_possible = true;
+$___phpsh___fork_every_command = false;
foreach (array_slice($GLOBALS['argv'], 3) as $___phpsh___arg) {
$___phpsh___did_arg = false;
if ($___phpsh___options_possible) {
@@ -54,6 +55,10 @@
$___phpsh___do_undefined_function_check = false;
$___phpsh___did_arg = true;
break;
+ case '-f':
+ $___phpsh___fork_every_command = true;
+ $___phpsh___did_arg = true;
+ break;
case '--':
$___phpsh___options_possible = false;
$___phpsh___did_arg = true;
@@ -380,11 +385,12 @@ class ___Phpsh___ {
* @author dcorson
*/
function __construct($output_from_includes='', $do_color, $do_autocomplete,
- $do_undefined_function_check, $comm_filename) {
+ $do_undefined_function_check, $fork_every_command, $comm_filename) {
$this->_comm_handle = fopen($comm_filename, 'w');
$this->__send_autocomplete_identifiers($do_autocomplete);
$this->do_color = $do_color;
$this->do_undefined_function_check = $do_undefined_function_check;
+ $this->fork_every_command = $fork_every_command;
// now it's safe to send any output the includes generated
echo $output_from_includes;
fwrite($this->_comm_handle, "ready\n");
@@ -529,14 +535,38 @@ function interactive_loop() {
if ($this->do_color) {
echo "\033[33m"; // yellow
}
- try {
- $evalue = eval($buffer);
- } catch (Exception $e) {
- // unfortunately, almost all exceptions that aren't explicitly thrown
- // by users are uncatchable :(
- fwrite(STDERR, 'Uncaught exception: '.get_class($e).': '.
- $e->getMessage()."\n");
+
+ if ($this->fork_every_command) {
+ $parent_pid = posix_getpid();
+ $pid = pcntl_fork();
$evalue = null;
+ if ($pid) {
+ pcntl_wait($status);
+ } else {
+ try {
+ $evalue = eval($buffer);
+ } catch (Exception $e) {
+ // unfortunately, almost all exceptions that aren't explicitly
+ // thrown by users are uncatchable :(
+ fwrite(STDERR, 'Uncaught exception: '.get_class($e).': '.
+ $e->getMessage()."\n");
+ $evalue = null;
+ }
+
+ // if we are still alive..
+ $childpid = posix_getpid();
+ fwrite($this->_comm_handle, "child $childpid\n");
+ }
+ } else {
+ try {
+ $evalue = eval($buffer);
+ } catch (Exception $e) {
+ // unfortunately, almost all exceptions that aren't explicitly thrown
+ // by users are uncatchable :(
+ fwrite(STDERR, 'Uncaught exception: '.get_class($e).': '.
+ $e->getMessage()."\n");
+ $evalue = null;
+ }
}
if ($buffer != "xdebug_break();\n") {
@@ -564,9 +594,11 @@ function interactive_loop() {
$___phpsh___ = new ___Phpsh___($___phpsh___output_from_includes,
$___phpsh___do_color, $___phpsh___do_autocomplete,
- $___phpsh___do_undefined_function_check, $argv[1]);
+ $___phpsh___do_undefined_function_check, $___phpsh___fork_every_command,
+ $argv[1]);
unset($___phpsh___do_color);
unset($___phpsh___do_autocomplete);
unset($___phpsh___do_undefined_function_check);
+unset($___phpsh___fork_every_command);
$___phpsh___->interactive_loop();
View
67 src/tests.py
@@ -0,0 +1,67 @@
+import unittest
+from phpsh import PhpshState, line_encode, do_sugar
+
+
+class TestPhpsh(unittest.TestCase):
+
+ def setUp(self):
+ self.ps = PhpshState(
+ cmd_incs=set(),
+ do_color=False,
+ do_echo=False,
+ codebase_mode='none',
+ do_autocomplete=False,
+ do_ctags=False,
+ interactive=True,
+ with_xdebug=False,
+ verbose=False)
+
+ def php(self, line):
+ expr = line_encode(do_sugar(line)) + '\n'
+ return self.ps.do_expr(expr).strip()
+
+ def assertPhp(self, line, expected=''):
+ self.assertEqual(str(expected), self.php(line))
+
+ def test_simple(self):
+ self.assertPhp('=1+1', 2)
+ self.assertPhp('$a=3')
+ self.assertPhp('=$a+2', 5)
+
+ def test_long_running_func(self):
+ func = ("function foo(){ "
+ " $a=0;"
+ " for ($i=0; $i<100000; $i++)"
+ " $a+=1;"
+ " return $a;"
+ "}")
+ self.php(func)
+ self.assertPhp('=foo()', '100000')
+
+ def test_undefined_func_check(self):
+ # make sure state is maintained after undefined function call
+ # is avoided.
+ self.assertPhp('$a=3')
+ self.assertPhp(
+ 'does_not_exist()',
+ 'Not executing input: Possible call to undefined '
+ 'function does_not_exist()\n'
+ 'See /etc/phpsh/config.sample to disable UndefinedFunctionCheck.')
+ self.assertPhp('=$a', 3)
+
+ def test_fatal(self):
+ # create some state
+ self.assertPhp('$a=3')
+
+ # run a function that does not exist (and get around the simple check)
+ self.assertPhp("$func = 'does_not_exist'")
+ result = self.php('$func()')
+ expected = 'Fatal error: Call to undefined function does_not_exist()'
+ self.assertTrue(expected in result)
+
+ # verify that the state is still there.
+ self.assertPhp('=$a', 3)
+
+
+if __name__ == '__main__':
+ unittest.main()
Please sign in to comment.
Something went wrong with that request. Please try again.