daemon.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. # Copyright 2019-present MongoDB, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Support for spawning a daemon process.
  15. PyMongo only attempts to spawn the mongocryptd daemon process when automatic
  16. client-side field level encryption is enabled. See
  17. :ref:`automatic-client-side-encryption` for more info.
  18. """
  19. import os
  20. import subprocess
  21. import sys
  22. import time
  23. import warnings
  24. # The maximum amount of time to wait for the intermediate subprocess.
  25. _WAIT_TIMEOUT = 10
  26. _THIS_FILE = os.path.realpath(__file__)
  27. if sys.version_info[0] < 3:
  28. def _popen_wait(popen, timeout):
  29. """Implement wait timeout support for Python 2."""
  30. from pymongo.monotonic import time as _time
  31. deadline = _time() + timeout
  32. # Initial delay of 1ms
  33. delay = .0005
  34. while True:
  35. returncode = popen.poll()
  36. if returncode is not None:
  37. return returncode
  38. remaining = deadline - _time()
  39. if remaining <= 0:
  40. # Just return None instead of raising an error.
  41. return None
  42. delay = min(delay * 2, remaining, .5)
  43. time.sleep(delay)
  44. else:
  45. def _popen_wait(popen, timeout):
  46. """Implement wait timeout support for Python 3."""
  47. try:
  48. return popen.wait(timeout=timeout)
  49. except subprocess.TimeoutExpired:
  50. # Silence TimeoutExpired errors.
  51. return None
  52. def _silence_resource_warning(popen):
  53. """Silence Popen's ResourceWarning.
  54. Note this should only be used if the process was created as a daemon.
  55. """
  56. # Set the returncode to avoid this warning when popen is garbage collected:
  57. # "ResourceWarning: subprocess XXX is still running".
  58. # See https://bugs.python.org/issue38890 and
  59. # https://bugs.python.org/issue26741.
  60. # popen is None when mongocryptd spawning fails
  61. if popen is not None:
  62. popen.returncode = 0
  63. if sys.platform == 'win32':
  64. # On Windows we spawn the daemon process simply by using DETACHED_PROCESS.
  65. _DETACHED_PROCESS = getattr(subprocess, 'DETACHED_PROCESS', 0x00000008)
  66. def _spawn_daemon(args):
  67. """Spawn a daemon process (Windows)."""
  68. try:
  69. with open(os.devnull, 'r+b') as devnull:
  70. popen = subprocess.Popen(
  71. args,
  72. creationflags=_DETACHED_PROCESS,
  73. stdin=devnull, stderr=devnull, stdout=devnull)
  74. _silence_resource_warning(popen)
  75. except FileNotFoundError as exc:
  76. warnings.warn('Failed to start %s: is it on your $PATH?\n'
  77. 'Original exception: %s' % (args[0], exc),
  78. RuntimeWarning, stacklevel=2)
  79. else:
  80. # On Unix we spawn the daemon process with a double Popen.
  81. # 1) The first Popen runs this file as a Python script using the current
  82. # interpreter.
  83. # 2) The script then decouples itself and performs the second Popen to
  84. # spawn the daemon process.
  85. # 3) The original process waits up to 10 seconds for the script to exit.
  86. #
  87. # Note that we do not call fork() directly because we want this procedure
  88. # to be safe to call from any thread. Using Popen instead of fork also
  89. # avoids triggering the application's os.register_at_fork() callbacks when
  90. # we spawn the mongocryptd daemon process.
  91. def _spawn(args):
  92. """Spawn the process and silence stdout/stderr."""
  93. try:
  94. with open(os.devnull, 'r+b') as devnull:
  95. return subprocess.Popen(
  96. args,
  97. close_fds=True,
  98. stdin=devnull, stderr=devnull, stdout=devnull)
  99. except FileNotFoundError as exc:
  100. warnings.warn('Failed to start %s: is it on your $PATH?\n'
  101. 'Original exception: %s' % (args[0], exc),
  102. RuntimeWarning, stacklevel=2)
  103. def _spawn_daemon_double_popen(args):
  104. """Spawn a daemon process using a double subprocess.Popen."""
  105. spawner_args = [sys.executable, _THIS_FILE]
  106. spawner_args.extend(args)
  107. temp_proc = subprocess.Popen(spawner_args, close_fds=True)
  108. # Reap the intermediate child process to avoid creating zombie
  109. # processes.
  110. _popen_wait(temp_proc, _WAIT_TIMEOUT)
  111. def _spawn_daemon(args):
  112. """Spawn a daemon process (Unix)."""
  113. # "If Python is unable to retrieve the real path to its executable,
  114. # sys.executable will be an empty string or None".
  115. if sys.executable:
  116. _spawn_daemon_double_popen(args)
  117. else:
  118. # Fallback to spawn a non-daemon process without silencing the
  119. # resource warning. We do not use fork here because it is not
  120. # safe to call from a thread on all systems.
  121. # Unfortunately, this means that:
  122. # 1) If the parent application is killed via Ctrl-C, the
  123. # non-daemon process will also be killed.
  124. # 2) Each non-daemon process will hang around as a zombie process
  125. # until the main application exits.
  126. _spawn(args)
  127. if __name__ == '__main__':
  128. # Attempt to start a new session to decouple from the parent.
  129. if hasattr(os, 'setsid'):
  130. try:
  131. os.setsid()
  132. except OSError:
  133. pass
  134. # We are performing a double fork (Popen) to spawn the process as a
  135. # daemon so it is safe to ignore the resource warning.
  136. _silence_resource_warning(_spawn(sys.argv[1:]))
  137. os._exit(0)