skywalking/bootstrap/cli/utility/runner.py (70 lines of code) (raw):
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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.
#
""" User application command runner """
import logging
import os
import platform
import sys
from typing import List
from skywalking.bootstrap import cli_logger
from skywalking.bootstrap.cli import SWRunnerFailure
def prefork_handler(command: List[str]) -> None:
"""
This handles the cases where pre-forking servers are EXPLICITLY used:
- gunicorn
- uwsgi
This handler only covers many plain usages, there could be cases where
gunicorn/uwsgi is loaded by other scripts and the envvars used here
are lost in such flow. (Aka. not covered by this handler)
"""
os.environ['prefork'] = ''
if command[0] == 'gunicorn':
# Maybe should also check 1: since there could be a command before gunicorn
cli_logger.info('We noticed you are using Gunicorn, '
'agent will automatically start the SkyWalking Python Agent'
'in all child (worker) processes and the master.')
os.environ['prefork'] = 'gunicorn'
elif command[0] == 'uwsgi':
cli_logger.info('We noticed you are using uWSGI, '
'agent will automatically add the following '
'environment variables to your uWSGI options (to ensure a functional Python agent): '
'--enable-threads --master. \n'
'We will also start the SkyWalking Python Agent in all child (worker) '
'processes except for the master.')
if '--master' not in command[1:]:
cli_logger.warning('No master process is specified, '
'agent will not start properly in workers, '
'automatically adding --master to your uwsgi command.')
os.environ['UWSGI_MASTER'] = 'true'
if '--enable-threads' not in command[1:]:
...
cli_logger.warning('No --enable-threads is specified, '
'agent will not start properly in workers, '
'automatically adding --enable-threads to your uwsgi command.')
os.environ['UWSGI_ENABLE_THREADS'] = 'true'
# this sets the option --import to our custom uwsgidecorator.postfork() function
# which is in loader/uw.py
os.environ['prefork'] = 'uwsgi'
# let's hope no one uses up all 4 env variables, shared-python-import
# shared-import, shared-pyimport, shared-py-import all imports in all processes no matter lazy/lazy-apps
def pick_env_var():
for env_var in ['UWSGI_SHARED_PYTHON_IMPORT',
'UWSGI_SHARED_IMPORT',
'UWSGI_SHARED_PYIMPORT',
'UWSGI_SHARED_PY_IMPORT']:
if env_var not in os.environ:
return env_var
raise SWRunnerFailure('No available env variable slot for sw-python to inject postfork hook, '
'agent will not start properly in workers, please unset one of your env variables or '
'fall back to manual postfork hook with @postfork.')
os.environ[pick_env_var()] = 'uwsgi_hook'
def execute(command: List[str], experimental_check_prefork: bool) -> None:
""" Set up environ and invokes the given command to replace current process """
cli_logger.debug(f'SkyWalking Python agent `runner` received command {command}')
if experimental_check_prefork:
cli_logger.info('Detected experimental prefork support flag, checking for pre-forking servers...')
prefork_handler(command=command)
cli_logger.debug('Adding sitecustomize.py to PYTHONPATH')
from skywalking.bootstrap.loader import __file__ as loader_dir
from skywalking.bootstrap.hooks import __file__ as hook_dir
loader_path = os.path.dirname(loader_dir)
hook_path = os.path.dirname(hook_dir)
new_path: str = ''
python_path = os.environ.get('PYTHONPATH')
if python_path: # If there is already a different PYTHONPATH, PREPEND to it as we must get loaded first.
partitioned = python_path.split(os.path.pathsep)
if loader_path not in partitioned: # check if we are already there
new_path = os.path.pathsep.join([loader_path, hook_path, python_path])
# When constructing sys.path PYTHONPATH is always
# before other paths and after interpreter invoker path, which is here or none
os.environ['PYTHONPATH'] = new_path if new_path else os.path.pathsep.join([loader_path, hook_path])
cli_logger.debug(f"Updated PYTHONPATH - {os.environ['PYTHONPATH']}")
# Used in sitecustomize to compare command's Python installation with CLI
# If not match, need to stop agent from loading, and kill the process
os.environ['SW_PYTHON_PREFIX'] = os.path.realpath(os.path.normpath(sys.prefix))
os.environ['SW_PYTHON_VERSION'] = platform.python_version()
# Pass down the logger debug setting to the replaced process, need a new logger there
os.environ['SW_AGENT_SW_PYTHON_CLI_DEBUG_ENABLED'] = 'True' if cli_logger.level == logging.DEBUG else 'False'
try:
cli_logger.info(f'New process starting with command - `{command[0]}` args - `{command}`')
os.execvp(command[0], command)
except OSError:
raise SWRunnerFailure