| 
12 | 12 | import sys  | 
13 | 13 | import sysconfig  | 
14 | 14 | import test.support  | 
 | 15 | +from io import StringIO  | 
 | 16 | +from unittest import mock  | 
15 | 17 | from test import support  | 
16 | 18 | from test.support import os_helper  | 
17 | 19 | from test.support.script_helper import assert_python_ok, assert_python_failure  | 
18 | 20 | from test.support import threading_helper  | 
19 | 21 | from test.support import import_helper  | 
20 | 22 | from test.support import force_not_colorized  | 
 | 23 | +from test.support import SHORT_TIMEOUT  | 
21 | 24 | try:  | 
22 | 25 |     from test.support import interpreters  | 
23 | 26 | except ImportError:  | 
@@ -1923,5 +1926,193 @@ def write(self, s):  | 
1923 | 1926 |         self.assertEqual(out, b"")  | 
1924 | 1927 |         self.assertEqual(err, b"")  | 
1925 | 1928 | 
 
  | 
 | 1929 | + | 
 | 1930 | +def _supports_remote_attaching():  | 
 | 1931 | +    PROCESS_VM_READV_SUPPORTED = False  | 
 | 1932 | + | 
 | 1933 | +    try:  | 
 | 1934 | +        from _testexternalinspection import PROCESS_VM_READV_SUPPORTED  | 
 | 1935 | +    except ImportError:  | 
 | 1936 | +        pass  | 
 | 1937 | + | 
 | 1938 | +    return PROCESS_VM_READV_SUPPORTED  | 
 | 1939 | + | 
 | 1940 | +@unittest.skipIf(not sys.is_remote_debug_enabled(), "Remote debugging is not enabled")  | 
 | 1941 | +@unittest.skipIf(sys.platform != "darwin" and sys.platform != "linux",  | 
 | 1942 | +                    "Test only runs on Linux and MacOS")  | 
 | 1943 | +@unittest.skipIf(sys.platform == "linux" and not _supports_remote_attaching(),  | 
 | 1944 | +                    "Test only runs on Linux with process_vm_readv support")  | 
 | 1945 | +class TestRemoteExec(unittest.TestCase):  | 
 | 1946 | +    def tearDown(self):  | 
 | 1947 | +        test.support.reap_children()  | 
 | 1948 | + | 
 | 1949 | +    def _run_remote_exec_test(self, script_code, python_args=None, env=None, prologue=''):  | 
 | 1950 | +        # Create the script that will be remotely executed  | 
 | 1951 | +        script = os_helper.TESTFN + '_remote.py'  | 
 | 1952 | +        self.addCleanup(os_helper.unlink, script)  | 
 | 1953 | + | 
 | 1954 | +        with open(script, 'w') as f:  | 
 | 1955 | +            f.write(script_code)  | 
 | 1956 | + | 
 | 1957 | +        # Create and run the target process  | 
 | 1958 | +        target = os_helper.TESTFN + '_target.py'  | 
 | 1959 | +        self.addCleanup(os_helper.unlink, target)  | 
 | 1960 | + | 
 | 1961 | +        with os_helper.temp_dir() as work_dir:  | 
 | 1962 | +            fifo = f"{work_dir}/the_fifo"  | 
 | 1963 | +            os.mkfifo(fifo)  | 
 | 1964 | +            self.addCleanup(os_helper.unlink, fifo)  | 
 | 1965 | + | 
 | 1966 | +            with open(target, 'w') as f:  | 
 | 1967 | +                f.write(f'''  | 
 | 1968 | +import sys  | 
 | 1969 | +import time  | 
 | 1970 | +
  | 
 | 1971 | +with open("{fifo}", "w") as fifo:  | 
 | 1972 | +    fifo.write("ready")  | 
 | 1973 | +
  | 
 | 1974 | +{prologue}  | 
 | 1975 | +
  | 
 | 1976 | +print("Target process running...")  | 
 | 1977 | +
  | 
 | 1978 | +# Wait for remote script to be executed  | 
 | 1979 | +# (the execution will happen as the following  | 
 | 1980 | +# code is processed as soon as the read() call  | 
 | 1981 | +# unblocks)  | 
 | 1982 | +with open("{fifo}", "r") as fifo:  | 
 | 1983 | +    fifo.read()  | 
 | 1984 | +
  | 
 | 1985 | +# Write confirmation back  | 
 | 1986 | +with open("{fifo}", "w") as fifo:  | 
 | 1987 | +    fifo.write("executed")  | 
 | 1988 | +''')  | 
 | 1989 | + | 
 | 1990 | +            # Start the target process and capture its output  | 
 | 1991 | +            cmd = [sys.executable]  | 
 | 1992 | +            if python_args:  | 
 | 1993 | +                cmd.extend(python_args)  | 
 | 1994 | +            cmd.append(target)  | 
 | 1995 | + | 
 | 1996 | +            with subprocess.Popen(cmd,  | 
 | 1997 | +                                  stdout=subprocess.PIPE,  | 
 | 1998 | +                                  stderr=subprocess.PIPE,  | 
 | 1999 | +                                  env=env) as proc:  | 
 | 2000 | +                try:  | 
 | 2001 | +                    # Wait for process to be ready  | 
 | 2002 | +                    with open(fifo, "r") as fifo_file:  | 
 | 2003 | +                        response = fifo_file.read()  | 
 | 2004 | +                    self.assertEqual(response, "ready")  | 
 | 2005 | + | 
 | 2006 | +                    # Try remote exec on the target process  | 
 | 2007 | +                    sys.remote_exec(proc.pid, script)  | 
 | 2008 | + | 
 | 2009 | +                    # Signal script to continue  | 
 | 2010 | +                    with open(fifo, "w") as fifo_file:  | 
 | 2011 | +                        fifo_file.write("continue")  | 
 | 2012 | + | 
 | 2013 | +                    # Wait for execution confirmation  | 
 | 2014 | +                    with open(fifo, "r") as fifo_file:  | 
 | 2015 | +                        response = fifo_file.read()  | 
 | 2016 | +                    self.assertEqual(response, "executed")  | 
 | 2017 | + | 
 | 2018 | +                    # Return output for test verification  | 
 | 2019 | +                    stdout, stderr = proc.communicate(timeout=1.0)  | 
 | 2020 | +                    return proc.returncode, stdout, stderr  | 
 | 2021 | +                except PermissionError:  | 
 | 2022 | +                    self.skipTest("Insufficient permissions to execute code in remote process")  | 
 | 2023 | +                finally:  | 
 | 2024 | +                    proc.kill()  | 
 | 2025 | +                    proc.terminate()  | 
 | 2026 | +                    proc.wait(timeout=SHORT_TIMEOUT)  | 
 | 2027 | + | 
 | 2028 | +    def test_remote_exec(self):  | 
 | 2029 | +        """Test basic remote exec functionality"""  | 
 | 2030 | +        script = '''  | 
 | 2031 | +print("Remote script executed successfully!")  | 
 | 2032 | +'''  | 
 | 2033 | +        returncode, stdout, stderr = self._run_remote_exec_test(script)  | 
 | 2034 | +        self.assertEqual(returncode, 0)  | 
 | 2035 | +        self.assertIn(b"Remote script executed successfully!", stdout)  | 
 | 2036 | +        self.assertEqual(stderr, b"")  | 
 | 2037 | + | 
 | 2038 | +    def test_remote_exec_with_self_process(self):  | 
 | 2039 | +        """Test remote exec with the target process being the same as the test process"""  | 
 | 2040 | + | 
 | 2041 | +        code = 'import sys;print("Remote script executed successfully!", file=sys.stderr)'  | 
 | 2042 | +        file = os_helper.TESTFN + '_remote.py'  | 
 | 2043 | +        with open(file, 'w') as f:  | 
 | 2044 | +            f.write(code)  | 
 | 2045 | +        self.addCleanup(os_helper.unlink, file)  | 
 | 2046 | +        with mock.patch('sys.stderr', new_callable=StringIO) as mock_stderr:  | 
 | 2047 | +            with mock.patch('sys.stdout', new_callable=StringIO) as mock_stdout:  | 
 | 2048 | +                sys.remote_exec(os.getpid(), file)  | 
 | 2049 | +                print("Done")  | 
 | 2050 | +                self.assertEqual(mock_stderr.getvalue(), "Remote script executed successfully!\n")  | 
 | 2051 | +                self.assertEqual(mock_stdout.getvalue(), "Done\n")  | 
 | 2052 | + | 
 | 2053 | +    def test_remote_exec_raises_audit_event(self):  | 
 | 2054 | +        """Test remote exec raises an audit event"""  | 
 | 2055 | +        prologue = '''\  | 
 | 2056 | +import sys  | 
 | 2057 | +def audit_hook(event, arg):  | 
 | 2058 | +    print(f"Audit event: {event}, arg: {arg}")  | 
 | 2059 | +sys.addaudithook(audit_hook)  | 
 | 2060 | +'''  | 
 | 2061 | +        script = '''  | 
 | 2062 | +print("Remote script executed successfully!")  | 
 | 2063 | +'''  | 
 | 2064 | +        returncode, stdout, stderr = self._run_remote_exec_test(script, prologue=prologue)  | 
 | 2065 | +        self.assertEqual(returncode, 0)  | 
 | 2066 | +        self.assertIn(b"Remote script executed successfully!", stdout)  | 
 | 2067 | +        self.assertIn(b"Audit event: remote_debugger_script, arg: ", stdout)  | 
 | 2068 | +        self.assertEqual(stderr, b"")  | 
 | 2069 | + | 
 | 2070 | +    def test_remote_exec_with_exception(self):  | 
 | 2071 | +        """Test remote exec with an exception raised in the target process  | 
 | 2072 | +
  | 
 | 2073 | +        The exception should be raised in the main thread of the target process  | 
 | 2074 | +        but not crash the target process.  | 
 | 2075 | +        """  | 
 | 2076 | +        script = '''  | 
 | 2077 | +raise Exception("Remote script exception")  | 
 | 2078 | +'''  | 
 | 2079 | +        returncode, stdout, stderr = self._run_remote_exec_test(script)  | 
 | 2080 | +        self.assertEqual(returncode, 0)  | 
 | 2081 | +        self.assertIn(b"Remote script exception", stderr)  | 
 | 2082 | +        self.assertEqual(stdout, b"Target process running...\n")  | 
 | 2083 | + | 
 | 2084 | +    def test_remote_exec_disabled_by_env(self):  | 
 | 2085 | +        """Test remote exec is disabled when PYTHON_DISABLE_REMOTE_DEBUG is set"""  | 
 | 2086 | +        env = os.environ.copy()  | 
 | 2087 | +        env['PYTHON_DISABLE_REMOTE_DEBUG'] = '1'  | 
 | 2088 | +        with self.assertRaisesRegex(RuntimeError, "Remote debugging is not enabled in the remote process"):  | 
 | 2089 | +            self._run_remote_exec_test("print('should not run')", env=env)  | 
 | 2090 | + | 
 | 2091 | +    def test_remote_exec_disabled_by_xoption(self):  | 
 | 2092 | +        """Test remote exec is disabled with -Xdisable-remote-debug"""  | 
 | 2093 | +        with self.assertRaisesRegex(RuntimeError, "Remote debugging is not enabled in the remote process"):  | 
 | 2094 | +            self._run_remote_exec_test("print('should not run')", python_args=['-Xdisable-remote-debug'])  | 
 | 2095 | + | 
 | 2096 | +    def test_remote_exec_invalid_pid(self):  | 
 | 2097 | +        """Test remote exec with invalid process ID"""  | 
 | 2098 | +        with self.assertRaises(OSError):  | 
 | 2099 | +            sys.remote_exec(999999, "print('should not run')")  | 
 | 2100 | + | 
 | 2101 | +    def test_remote_exec_syntax_error(self):  | 
 | 2102 | +        """Test remote exec with syntax error in script"""  | 
 | 2103 | +        script = '''  | 
 | 2104 | +this is invalid python code  | 
 | 2105 | +'''  | 
 | 2106 | +        returncode, stdout, stderr = self._run_remote_exec_test(script)  | 
 | 2107 | +        self.assertEqual(returncode, 0)  | 
 | 2108 | +        self.assertIn(b"SyntaxError", stderr)  | 
 | 2109 | +        self.assertEqual(stdout, b"Target process running...\n")  | 
 | 2110 | + | 
 | 2111 | +    def test_remote_exec_invalid_script_path(self):  | 
 | 2112 | +        """Test remote exec with invalid script path"""  | 
 | 2113 | +        with self.assertRaises(OSError):  | 
 | 2114 | +            sys.remote_exec(os.getpid(), "invalid_script_path")  | 
 | 2115 | + | 
 | 2116 | + | 
1926 | 2117 | if __name__ == "__main__":  | 
1927 | 2118 |     unittest.main()  | 
0 commit comments