_child.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. # -----------------------------------------------------------------------------
  2. # Copyright (c) 2021-2023, PyInstaller Development Team.
  3. #
  4. # Distributed under the terms of the GNU General Public License (version 2
  5. # or later) or, at the user's discretion, the MIT License.
  6. #
  7. # The full license is in the file COPYING.txt, distributed with this software.
  8. #
  9. # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception OR MIT)
  10. # -----------------------------------------------------------------------------
  11. """
  12. The child process to be invoked by IsolatedPython().
  13. This file is to be run directly with pipe handles for reading from and writing to the parent process as command line
  14. arguments.
  15. """
  16. import sys
  17. import os
  18. import types
  19. from marshal import loads, dumps
  20. from base64 import b64encode, b64decode
  21. from traceback import format_exception
  22. if os.name == "nt":
  23. from msvcrt import open_osfhandle
  24. def _open(osf_handle, mode):
  25. # Convert system file handles to file descriptors before opening them.
  26. return open(open_osfhandle(osf_handle, 0), mode)
  27. else:
  28. _open = open
  29. def run_next_command(read_fh, write_fh):
  30. """
  31. Listen to **read_fh** for the next function to run. Write the result to **write_fh**.
  32. """
  33. # Check the first line of input. Receiving an empty line is the signal that there are no more tasks to be ran.
  34. first_line = read_fh.readline()
  35. if first_line == b"\n":
  36. # It's time to end this child process
  37. return False
  38. # There are 5 lines to read: The function's code, its default args, its default kwargs, its args, and its kwargs.
  39. code = loads(b64decode(first_line.strip()))
  40. _defaults = loads(b64decode(read_fh.readline().strip()))
  41. _kwdefaults = loads(b64decode(read_fh.readline().strip()))
  42. args = loads(b64decode(read_fh.readline().strip()))
  43. kwargs = loads(b64decode(read_fh.readline().strip()))
  44. try:
  45. # Define the global namespace available to the function.
  46. GLOBALS = {"__builtins__": __builtins__, "__isolated__": True}
  47. # Reconstruct the function.
  48. function = types.FunctionType(code, GLOBALS)
  49. function.__defaults__ = _defaults
  50. function.__kwdefaults__ = _kwdefaults
  51. # Run it.
  52. output = function(*args, **kwargs)
  53. # Verify that the output is serialise-able (i.e. no custom types or module or function references) here so that
  54. # it's caught if it fails.
  55. marshalled = dumps((True, output))
  56. except BaseException as ex:
  57. # An exception happened whilst either running the function or serialising its output. Send back a string
  58. # version of the traceback (unfortunately raw traceback objects are not marshal-able) and a boolean to say
  59. # that it failed.
  60. tb_lines = format_exception(type(ex), ex, ex.__traceback__)
  61. if tb_lines[0] == "Traceback (most recent call last):\n":
  62. # This particular line is distracting. Get rid of it.
  63. tb_lines = tb_lines[1:]
  64. marshalled = dumps((False, "".join(tb_lines).rstrip()))
  65. # Send the output (return value or traceback) back to the parent.
  66. write_fh.write(b64encode(marshalled))
  67. write_fh.write(b"\n")
  68. write_fh.flush()
  69. # Signal that an instruction was ran (successfully or otherwise).
  70. return True
  71. if __name__ == '__main__':
  72. # Mark this process as PyInstaller's isolated subprocess; this makes attempts at spawning further isolated
  73. # subprocesses via `PyInstaller.isolated` from this process no-op.
  74. sys._pyi_isolated_subprocess = True
  75. read_from_parent, write_to_parent = map(int, sys.argv[1:])
  76. with _open(read_from_parent, "rb") as read_fh:
  77. with _open(write_to_parent, "wb") as write_fh:
  78. sys.path = loads(b64decode(read_fh.readline()))
  79. # Keep receiving and running instructions until the parent sends the signal to stop.
  80. while run_next_command(read_fh, write_fh):
  81. pass