pyi_splash.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. # -----------------------------------------------------------------------------
  2. # Copyright (c) 2005-2021, PyInstaller Development Team.
  3. #
  4. # Distributed under the terms of the GNU General Public License (version 2
  5. # or later) with exception for distributing the bootloader.
  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)
  10. # -----------------------------------------------------------------------------
  11. # This module is not a "fake module" in the classical sense,
  12. # but a real module that can be imported. It acts as an RPC
  13. # interface for the functions of the bootloader.
  14. """
  15. This module connects to the bootloader to send messages to the splash screen.
  16. It is intended to act as a RPC interface for the functions provided by the
  17. bootloader, such as displaying text or closing. This makes the users python
  18. program independent of how the communication with the bootloader is
  19. implemented, since a consistent API is provided.
  20. To connect to the bootloader, it connects to a local tcp socket whose port is
  21. passed through the environment variable '_PYIBoot_SPLASH'. The bootloader
  22. creates a server socket and accepts every connection request. Since the
  23. os-module, which is needed to request the environment variable, is not
  24. available at boot time, the module does not establish the connection until
  25. initialization.
  26. The protocol by which the Python interpreter communicates with the bootloader
  27. is implemented in this module.
  28. This module does not support reloads while the splash screen is displayed, i.e.
  29. it cannot be reloaded (such as by importlib.reload), because the splash
  30. screen closes automatically when the connection to this instance of the
  31. module is lost.
  32. """
  33. import os
  34. import atexit
  35. # import the _socket module instead of the socket module.
  36. # All used functions to connect to the ipc system are provided
  37. # by the C module and the users program does not necessarily need to include
  38. # the socket module and all required module it uses.
  39. import _socket
  40. __all__ = ["CLOSE_CONNECTION", "FLUSH_CHARACTER",
  41. "is_alive", "close", "update_text"]
  42. try:
  43. # The user might have excluded logging from imports
  44. import logging as _logging
  45. except ImportError:
  46. _logging = None
  47. try:
  48. # The user might have excluded functools from imports
  49. from functools import update_wrapper
  50. except ImportError:
  51. update_wrapper = None
  52. # Utility
  53. def _log(level, msg, *args, **kwargs):
  54. """ Conditional wrapper around logging module.
  55. If the user excluded logging from the imports or it was not
  56. imported this module should handle it and dont log anything
  57. """
  58. if _logging:
  59. logger = _logging.getLogger(__name__)
  60. logger.log(level, msg, *args, **kwargs)
  61. # These constants define single characters which are needed to send
  62. # commands to the bootloader. Those constants are also set in the tcl script
  63. CLOSE_CONNECTION = b'\x04' # ASCII End-of-Transmission character
  64. FLUSH_CHARACTER = b'\x0D' # ASCII Carriage Return character
  65. # Module internal variables
  66. _initialized = False
  67. # Keep these variables always synchronized
  68. _ipc_socket_closed = True
  69. _ipc_socket = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
  70. def _initialize():
  71. """Initialize this module
  72. :return:
  73. """
  74. global _initialized, _ipc_socket, _ipc_socket_closed
  75. try:
  76. _ipc_socket.connect(("localhost", _ipc_port))
  77. _ipc_socket_closed = False
  78. _initialized = True
  79. _log(20, "A connection to the splash screen was"
  80. " successfully established.") # log-level: info
  81. except OSError as err:
  82. raise ConnectionError("Unable to connect to the tcp server socket"
  83. " on port %d" % _ipc_port) from err
  84. # We expect a splash screen from the bootloader, but if _PYIBoot_SPLASH
  85. # is not set, the module cannot connect to it.
  86. try:
  87. _ipc_port = int(os.environ['_PYIBoot_SPLASH'])
  88. del os.environ['_PYIBoot_SPLASH']
  89. # Initialize the connection upon importing this module.
  90. # This will establish a connection to the bootloader's tcp server socket
  91. _initialize()
  92. except (KeyError, ValueError) as _err:
  93. # log-level: warning
  94. _log(30, "The environment does not allow connecting to the"
  95. " splash screen. Are the splash resources attached"
  96. " to the bootloader or did an error occur?", exc_info=_err)
  97. except ConnectionError as _err:
  98. # log-level: error
  99. _log(40, "Cannot connect to the bootloaders ipc server socket",
  100. exc_info=_err)
  101. def _check_connection(func):
  102. """ Utility decorator for checking whether the function should be executed.
  103. The wrapped function may raise a ConnectionError if the module was not
  104. initialized correctly.
  105. """
  106. def wrapper(*args, **kwargs):
  107. """ Executes the wrapped function if the environment allows it.
  108. That is, if the connection to to bootloader has not been closed
  109. and the module is initialized.
  110. :raises RuntimeError: if the module was not initialized correctly.
  111. """
  112. if _initialized and _ipc_socket_closed:
  113. _log(20, "The module has been disabled, so the use of the splash"
  114. " screen is not longer supported.") # log-level: info
  115. return
  116. elif not _initialized:
  117. raise RuntimeError("This module is not initialized."
  118. " Did this module failed to load?")
  119. return func(*args, **kwargs)
  120. if update_wrapper:
  121. # For runtime introspection
  122. update_wrapper(wrapper, func)
  123. return wrapper
  124. @_check_connection
  125. def _send_command(cmd, args=None):
  126. """ Send the command followed by args to the splash screen
  127. :param str cmd: The command to send. All command have to be defined as
  128. procedures in the tcl splash screen script
  129. :param list[str] args: All arguments to send to the receiving function
  130. """
  131. if args is None:
  132. args = []
  133. full_cmd = "%s(%s)" % (cmd, " ".join(args))
  134. try:
  135. _ipc_socket.sendall(full_cmd.encode("utf-8") + FLUSH_CHARACTER)
  136. except OSError as err:
  137. raise ConnectionError(
  138. "Unable to send '%s' to the bootloader" % full_cmd) from err
  139. def is_alive():
  140. """ Indicates whether the module can be used.
  141. Returns False if the module is either not initialized ot was disabled
  142. by closing the splash screen. Otherwise, the module should be usable.
  143. """
  144. return _initialized and not _ipc_socket_closed
  145. @_check_connection
  146. def update_text(msg):
  147. """ Updates the text on the splash screen window.
  148. :param str msg: the text to be displayed
  149. :raises ConnectionError: If the OS fails to write to the socket
  150. :raises RuntimeError: If the module is not initialized
  151. """
  152. _send_command("update_text", [msg])
  153. def close():
  154. """Close the connection to the ipc tcp server socket
  155. This will close the splash screen and renders this module unusable.
  156. After this function is called, no connection can be opened to the splash
  157. screen again and all functions if this module become unusable
  158. """
  159. global _ipc_socket_closed
  160. if _initialized and not _ipc_socket_closed:
  161. _ipc_socket.sendall(CLOSE_CONNECTION)
  162. _ipc_socket.close()
  163. _ipc_socket_closed = True
  164. @atexit.register
  165. def _exit():
  166. close()
  167. # Discarded idea:
  168. # Problem:
  169. # There was a race condition between the tcl (splash screen) and
  170. # python interpreter. Initially the tcl was started as a separate thread next
  171. # to the bootloader thread, which starts python. Tcl sets the environment
  172. # variable '_PYIBoot_SPLASH' with a port to connect to. If the python
  173. # interpreter is faster initialized than the tcl interpreter (sometimes the
  174. # case in onedir mode) the environment variable does not yet exist. Since
  175. # python caches the environment variables at startup, updating the environ
  176. # from tcl does not update the python environ.
  177. #
  178. # Considered Solution:
  179. # Dont rely on python itself to look up the environment
  180. # variables. We could implement via ctypes functions to look up the latest
  181. # environ. See https://stackoverflow.com/a/33642551/5869139 for a possible
  182. # implementation.
  183. #
  184. # Discarded because:
  185. # This module would need to implement for every supported
  186. # OS a dll hook to link to the environ variable, technically reimplementing
  187. # the C function 'convertenviron' from posixmodule.c_ in python. The
  188. # implemented solution now waits for the tcl interpreter to finish before
  189. # starting python.
  190. #
  191. # .. _posixmodule.c:
  192. # https://github.com/python/cpython/blob/3.7/Modules/posixmodule.c#L1315-L1393