You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
143 lines
5.6 KiB
143 lines
5.6 KiB
# Copyright 2019-present MongoDB, Inc. |
|
# |
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
|
# you may not use this file except in compliance with the License. |
|
# You may obtain a copy of the License at |
|
# |
|
# http://www.apache.org/licenses/LICENSE-2.0 |
|
# |
|
# Unless required by applicable law or agreed to in writing, software |
|
# distributed under the License is distributed on an "AS IS" BASIS, |
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
# See the License for the specific language governing permissions and |
|
# limitations under the License. |
|
|
|
"""Support for spawning a daemon process. |
|
|
|
PyMongo only attempts to spawn the mongocryptd daemon process when automatic |
|
client-side field level encryption is enabled. See |
|
:ref:`automatic-client-side-encryption` for more info. |
|
""" |
|
|
|
import os |
|
import subprocess |
|
import sys |
|
import warnings |
|
from typing import Optional, Sequence |
|
|
|
# The maximum amount of time to wait for the intermediate subprocess. |
|
_WAIT_TIMEOUT = 10 |
|
_THIS_FILE = os.path.realpath(__file__) |
|
|
|
|
|
def _popen_wait(popen: subprocess.Popen, timeout: Optional[float]) -> Optional[int]: |
|
"""Implement wait timeout support for Python 3.""" |
|
try: |
|
return popen.wait(timeout=timeout) |
|
except subprocess.TimeoutExpired: |
|
# Silence TimeoutExpired errors. |
|
return None |
|
|
|
|
|
def _silence_resource_warning(popen: Optional[subprocess.Popen]) -> None: |
|
"""Silence Popen's ResourceWarning. |
|
|
|
Note this should only be used if the process was created as a daemon. |
|
""" |
|
# Set the returncode to avoid this warning when popen is garbage collected: |
|
# "ResourceWarning: subprocess XXX is still running". |
|
# See https://bugs.python.org/issue38890 and |
|
# https://bugs.python.org/issue26741. |
|
# popen is None when mongocryptd spawning fails |
|
if popen is not None: |
|
popen.returncode = 0 |
|
|
|
|
|
if sys.platform == "win32": |
|
# On Windows we spawn the daemon process simply by using DETACHED_PROCESS. |
|
_DETACHED_PROCESS = getattr(subprocess, "DETACHED_PROCESS", 0x00000008) |
|
|
|
def _spawn_daemon(args: Sequence[str]) -> None: |
|
"""Spawn a daemon process (Windows).""" |
|
try: |
|
with open(os.devnull, "r+b") as devnull: |
|
popen = subprocess.Popen( |
|
args, |
|
creationflags=_DETACHED_PROCESS, |
|
stdin=devnull, |
|
stderr=devnull, |
|
stdout=devnull, |
|
) |
|
_silence_resource_warning(popen) |
|
except FileNotFoundError as exc: |
|
warnings.warn( |
|
f"Failed to start {args[0]}: is it on your $PATH?\nOriginal exception: {exc}", |
|
RuntimeWarning, |
|
stacklevel=2, |
|
) |
|
|
|
else: |
|
# On Unix we spawn the daemon process with a double Popen. |
|
# 1) The first Popen runs this file as a Python script using the current |
|
# interpreter. |
|
# 2) The script then decouples itself and performs the second Popen to |
|
# spawn the daemon process. |
|
# 3) The original process waits up to 10 seconds for the script to exit. |
|
# |
|
# Note that we do not call fork() directly because we want this procedure |
|
# to be safe to call from any thread. Using Popen instead of fork also |
|
# avoids triggering the application's os.register_at_fork() callbacks when |
|
# we spawn the mongocryptd daemon process. |
|
def _spawn(args: Sequence[str]) -> Optional[subprocess.Popen]: |
|
"""Spawn the process and silence stdout/stderr.""" |
|
try: |
|
with open(os.devnull, "r+b") as devnull: |
|
return subprocess.Popen( |
|
args, close_fds=True, stdin=devnull, stderr=devnull, stdout=devnull |
|
) |
|
except FileNotFoundError as exc: |
|
warnings.warn( |
|
f"Failed to start {args[0]}: is it on your $PATH?\nOriginal exception: {exc}", |
|
RuntimeWarning, |
|
stacklevel=2, |
|
) |
|
return None |
|
|
|
def _spawn_daemon_double_popen(args: Sequence[str]) -> None: |
|
"""Spawn a daemon process using a double subprocess.Popen.""" |
|
spawner_args = [sys.executable, _THIS_FILE] |
|
spawner_args.extend(args) |
|
temp_proc = subprocess.Popen(spawner_args, close_fds=True) |
|
# Reap the intermediate child process to avoid creating zombie |
|
# processes. |
|
_popen_wait(temp_proc, _WAIT_TIMEOUT) |
|
|
|
def _spawn_daemon(args: Sequence[str]) -> None: |
|
"""Spawn a daemon process (Unix).""" |
|
# "If Python is unable to retrieve the real path to its executable, |
|
# sys.executable will be an empty string or None". |
|
if sys.executable: |
|
_spawn_daemon_double_popen(args) |
|
else: |
|
# Fallback to spawn a non-daemon process without silencing the |
|
# resource warning. We do not use fork here because it is not |
|
# safe to call from a thread on all systems. |
|
# Unfortunately, this means that: |
|
# 1) If the parent application is killed via Ctrl-C, the |
|
# non-daemon process will also be killed. |
|
# 2) Each non-daemon process will hang around as a zombie process |
|
# until the main application exits. |
|
_spawn(args) |
|
|
|
if __name__ == "__main__": |
|
# Attempt to start a new session to decouple from the parent. |
|
if hasattr(os, "setsid"): |
|
try: |
|
os.setsid() |
|
except OSError: |
|
pass |
|
|
|
# We are performing a double fork (Popen) to spawn the process as a |
|
# daemon so it is safe to ignore the resource warning. |
|
_silence_resource_warning(_spawn(sys.argv[1:])) |
|
os._exit(0)
|
|
|