diff --git a/interpreter/core/computer/terminal/languages/php.py b/interpreter/core/computer/terminal/languages/php.py new file mode 100644 index 0000000000..6285317c69 --- /dev/null +++ b/interpreter/core/computer/terminal/languages/php.py @@ -0,0 +1,66 @@ +import os + +from .subprocess_language import SubprocessLanguage + + +class Php(SubprocessLanguage): + file_extension = "php" + name = "PHP" + aliases = ["php"] + + def __init__( + self, + ): + super().__init__() + self.close_stdin = True + self.start_cmd = ["php"] + + def preprocess_code(self, code): + """ + Add active line markers + Wrap in a try except (trap in shell) + Add end of execution marker + """ + lines = code.split("\n") + + if lines[0] == '': + # remove empty line at the start + lines.pop(0) + + if lines[-1] == '': + # remove empty line at the end + lines.pop(-1) + + if lines[-1] == '?>': + # remove close tag at the end + lines.pop(-1) + + r_code = "" + for i, line in enumerate(lines, 1): + if os.environ.get("INTERPRETER_ACTIVE_LINE_DETECTION", "True").lower() == "true": + if -1 != line.find(''): + # Add commands that tell us what the active line is + r_code += f'echo "##active_line{i}##", PHP_EOL;\n' + r_code += f'{line}\n' + + # Add end command (we'll be listening for this so we know when it ends) + r_code += 'echo PHP_EOL, "##end_of_execution##", PHP_EOL;' + + return r_code + + def line_postprocessor(self, line): + return line + + def detect_active_line(self, line): + if "##active_line" in line: + return int(line.split("##active_line")[1].split("##")[0]) + return None + + def detect_end_of_execution(self, line): + return "##end_of_execution##" in line diff --git a/interpreter/core/computer/terminal/languages/subprocess_language.py b/interpreter/core/computer/terminal/languages/subprocess_language.py index dd422beb7f..af39313838 100644 --- a/interpreter/core/computer/terminal/languages/subprocess_language.py +++ b/interpreter/core/computer/terminal/languages/subprocess_language.py @@ -40,6 +40,7 @@ def terminate(self): self.process.terminate() self.process.stdin.close() self.process.stdout.close() + self.process.stderr.close() def start_process(self): if self.process: @@ -83,6 +84,7 @@ def run(self, code): yield { "type": "console", "format": "output", + "error": True, "content": traceback.format_exc(), } return @@ -95,7 +97,8 @@ def run(self, code): try: self.process.stdin.write(code + "\n") - self.process.stdin.flush() + # if execute PHP code with "flush()" it just hangs + self.process.stdin.close() break except: if retry_count != 0: @@ -105,6 +108,7 @@ def run(self, code): yield { "type": "console", "format": "output", + "error": True, "content": f"{traceback.format_exc()}\nRetrying... ({retry_count}/{max_retries})\nRestarting process.", } @@ -115,10 +119,12 @@ def run(self, code): yield { "type": "console", "format": "output", + "error": True, "content": "Maximum retries reached. Could not execute code.", } return + retry_count = 0 while True: if not self.output_queue.empty(): yield self.output_queue.get() @@ -136,6 +142,15 @@ def run(self, code): yield self.output_queue.get() time.sleep(0.2) break + retry_count += 1 + if retry_count > max_retries: + yield { + "type": "console", + "format": "output", + "error": True, + "content": "Maximum retries reached. Code is hang.", + } + return def handle_stream_output(self, stream, is_error_stream): try: diff --git a/interpreter/core/computer/terminal/terminal.py b/interpreter/core/computer/terminal/terminal.py index b9f92582f6..0c25e59a57 100644 --- a/interpreter/core/computer/terminal/terminal.py +++ b/interpreter/core/computer/terminal/terminal.py @@ -15,6 +15,7 @@ from .languages.react import React from .languages.ruby import Ruby from .languages.shell import Shell +from .languages.php import Php # Should this be renamed to OS or System? @@ -44,6 +45,7 @@ def __init__(self, computer): PowerShell, React, Java, + Php, ] self._active_languages = {} diff --git a/tests/core/computer/terminal/languages/test_php.py b/tests/core/computer/terminal/languages/test_php.py new file mode 100644 index 0000000000..11e79add06 --- /dev/null +++ b/tests/core/computer/terminal/languages/test_php.py @@ -0,0 +1,43 @@ +import unittest +import shutil +from interpreter.core.computer.terminal.languages.php import Php + +class TestPhp(unittest.TestCase): + def setUp(self): + if shutil.which("php") is None: + raise unittest.SkipTest("php not installed") + + self.php = Php() + + def tearDown(self): + self.php.terminate() + + def test_run(self): + for chunk in self.php.run("\n\n"): + if chunk["format"] == "active_line" or chunk["content"] == "\n": + pass + elif chunk["format"] == "output": + self.assertEqual('Hello World\n', chunk["content"]) + else: + self.fail('Wrong chunk format') + + def test_run_hang(self): + for chunk in self.php.run("\n\n"): + if chunk["format"] == "active_line" or chunk["content"] == "\n": + pass + elif "error" in chunk: + self.assertEqual("Maximum retries reached. Code is hang.", chunk["content"]) + elif chunk["format"] == "output": + self.assertEqual('Parse error: syntax error, unexpected string content ";", ' + 'expecting "," or ";" in Standard input code on line 3\n', chunk["content"]) + else: + self.fail('Wrong chunk format') + +if __name__ == "__main__": + testing = TestPhp() + testing.setUp() + testing.test_run() + testing.tearDown() + testing.setUp() + testing.test_run_hang() + testing.tearDown() diff --git a/tests/core/computer/terminal/languages/test_shell.py b/tests/core/computer/terminal/languages/test_shell.py new file mode 100644 index 0000000000..35065e6560 --- /dev/null +++ b/tests/core/computer/terminal/languages/test_shell.py @@ -0,0 +1,38 @@ +import unittest +from interpreter.core.computer.terminal.languages.shell import Shell + +class TestShell(unittest.TestCase): + def setUp(self): + self.shell = Shell() + + def tearDown(self): + self.shell.terminate() + + def test_run(self): + for chunk in self.shell.run("echo 'Hello World'"): + if chunk["format"] == "active_line" or chunk["content"] == "\n": + pass + elif chunk["format"] == "output": + self.assertEqual('Hello World\n', chunk["content"]) + else: + self.fail('Wrong chunk format') + + def test_run_hang(self): + for chunk in self.shell.run("echo World'"): + if chunk["format"] == "active_line" or chunk["content"] == "\n": + pass + elif "error" in chunk: + self.assertEqual("Maximum retries reached. Code is hang.", chunk["content"]) + elif chunk["format"] == "output": + self.assertIn('unmatched', chunk["content"]) + else: + self.fail('Wrong chunk format') + +if __name__ == "__main__": + testing = TestShell() + testing.setUp() + testing.test_run() + testing.tearDown() + testing.setUp() + testing.test_run_hang() + testing.tearDown()