Permalink
Browse files

phpsh recover from fatals without losing state

Summary:
based on pcarduner's idea of forking on every command and killing the
parent. Return new pid in the command file.
  • Loading branch information...
1 parent a1138c3 commit 06081d4daea1f179da78d06cea216a018d65763e Yiding Jia committed Jul 8, 2010
Showing with 104 additions and 15 deletions.
  1. +17 −7 src/__init__.py
  2. +20 −8 src/phpsh.php
  3. +67 −0 src/tests.py
View
@@ -726,9 +726,10 @@ 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:
+ while True:
# line-buffer stdout and stderr
if debug:
print "start loop"
@@ -747,8 +748,7 @@ 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
+ # process has died, will be dealt with in outer loop
break
out_buff[out_buff_i] += buff
last_nl_pos = out_buff[out_buff_i].rfind("\n")
@@ -763,9 +763,14 @@ 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"):
+ 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 +1006,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 +1018,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
@@ -529,14 +529,26 @@ 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");
- $evalue = null;
+
+ $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");
}
if ($buffer != "xdebug_break();\n") {
View
@@ -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()

0 comments on commit 06081d4

Please sign in to comment.