whatap-python 2.2.0__tar.gz → 2.2.1rc1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/PKG-INFO +1 -1
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/__init__.py +89 -4
- whatap_python-2.2.1rc1/whatap/build.py +4 -0
- whatap_python-2.2.1rc1/whatap/util/native_process.py +458 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap_python.egg-info/PKG-INFO +1 -1
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap_python.egg-info/SOURCES.txt +1 -0
- whatap_python-2.2.0/whatap/build.py +0 -4
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/README.md +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/pyproject.toml +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/setup.cfg +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/setup.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/LICENSE +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/README.rst +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/__main__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/agent/darwin/amd64/whatap_python +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/agent/darwin/arm64/whatap_python +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/agent/linux/amd64/whatap_python +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/agent/linux/arm64/whatap_python +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/agent/windows/whatap_python.exe +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/bootstrap/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/bootstrap/sitecustomize.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/conf/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/conf/configuration.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/conf/configure.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/conf/license.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/control/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/counter/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/counter/counter_manager.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/counter/tasks/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/counter/tasks/base_task.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/counter/tasks/llm_evaluator_task.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/counter/tasks/llm_log_sink_task.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/counter/tasks/llm_stat_task.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/counter/tasks/openfiledescriptor.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/io/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/io/data_inputx.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/io/data_outputx.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/api/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/api/agent_name.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/api/prompt_meta.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/api/session.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/api/turn.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/api/user.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/api/workflow.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/definitions.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/base.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/builtins/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/builtins/_json_repair.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/builtins/answer_relevance.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/builtins/combined_judge.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/builtins/factuality.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/builtins/hallucination.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/builtins/judge_factories.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/builtins/llm_judge.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/builtins/pii_leak.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/builtins/prompt_injection.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/builtins/toxicity.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/builtins/url_scan.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/registry.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/sampler.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/evaluators/scope.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/features.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/instrument/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/instrument/_boundary.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/instrument/agent.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/instrument/llm_api.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/instrument/workflow.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/log_sink_packs/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/log_sink_packs/llm_input_message.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/log_sink_packs/llm_log_sink_pack.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/log_sink_packs/llm_output_message.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/log_sink_packs/llm_step_eval_status.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/log_sink_packs/llm_step_status.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/log_sink_packs/llm_system_message.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/log_sink_packs/llm_tool_calls.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/log_sink_packs/llm_tool_results.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/log_sink_packs/llm_tx_status.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/orchestration/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/orchestration/hermes/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/orchestration/hermes/gateway.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/orchestration/hermes/kanban.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/orchestration/langgraph/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/pricing.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/anthropic/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/anthropic/messages/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/anthropic/messages/messages.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/anthropic/messages/messages_context.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/anthropic/messages/messages_extractor.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/anthropic/messages/messages_stream.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/interceptor.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/chat/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/chat/chat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/chat/chat_context.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/chat/chat_extractor.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/completions/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/completions/completions.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/completions/completions_context.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/completions/completions_extractor.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/content_parser.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/embeddings/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/embeddings/embeddings.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/embeddings/embeddings_context.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/embeddings/embeddings_extractor.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/responses/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/responses/responses.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/responses/responses_context.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/openai/responses/responses_extractor.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/stream_accumulator.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/providers/workflow.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/active_agent_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/active_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/actx_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/answer_relevance_eval_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/api_status_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/base_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/combined_judge_eval_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/error_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/eval_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/factuality_eval_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/feature_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/finish_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/hallucination_eval_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/perf_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/pii_leak_eval_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/prompt_injection_eval_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/token_usage_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/toxicity_eval_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/traj_actx_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/llm/stats/url_scan_eval_stat.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/net/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/net/async_sender.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/net/packet_enum.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/net/packet_type_enum.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/net/param_def.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/net/stackhelper.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/net/udp_session.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/net/udp_thread.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/pack/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/pack/logSinkPack.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/pack/pack.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/pack/pack_enum.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/pack/tagCountPack.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/scripts/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/amqp/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/amqp/kombu.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/amqp/pika.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/bottle.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/celery.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/cherrypy.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/django.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/django_asgi.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/django_py3.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/fastapi/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/fastapi/endpoint.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/fastapi/exception_log.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/fastapi/instrumentation.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/fastapi/scope.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/fastapi/transaction.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/flask.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/frappe.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/graphql.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/nameko.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/odoo.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/starlette.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/tornado.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/application/wsgi.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/database/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/database/cxoracle.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/database/mongo.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/database/mysql.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/database/neo4j.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/database/psycopg2.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/database/psycopg3.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/database/redis.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/database/sqlalchemy.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/database/sqlite3.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/database/util.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/email/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/email/smtp.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/hook_service_patterns.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/httpc/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/httpc/django.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/httpc/httplib.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/httpc/httpx.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/httpc/requests.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/httpc/urllib3.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/httpc/util.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/logging.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/pattern_targets.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/plugin.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/process/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/process/subprocess_spawn.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/standalone/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/standalone/multiple.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/standalone/single.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/thread_context.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mod/workflow_plugin.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/mtrace_env.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/patch.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/simple_trace_context.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/trace_context.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/trace_context_manager.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/trace_error.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/trace_handler.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/trace_import.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/trace/trace_module_definition.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/bit_util.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/cardinality/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/cardinality/hyperloglog.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/cardinality/murmurhash.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/cardinality/registerset.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/compare_util.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/date_util.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/debug_util.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/escape_literal_sql.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/frame_util.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/hash_util.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/hexa32.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/int_set.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/ip_util.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/keygen.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/linked_list.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/linked_map.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/metering_util.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/request_double_queue.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/request_queue.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/string_util.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/throttle_util.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/util/userid_util.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/__init__.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/blob_value.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/boolean_value.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/decimal_value.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/double_summary.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/double_value.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/float_array.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/float_value.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/int_array.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/ip4_value.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/list_value.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/long_array.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/long_summary.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/map_value.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/null_value.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/number_value.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/summary_value.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/text_array.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/text_hash_value.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/text_value.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/value.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/value/value_enum.py +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap/whatap.conf +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap_python.egg-info/dependency_links.txt +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap_python.egg-info/entry_points.txt +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap_python.egg-info/not-zip-safe +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap_python.egg-info/requires.txt +0 -0
- {whatap_python-2.2.0 → whatap_python-2.2.1rc1}/whatap_python.egg-info/top_level.txt +0 -0
|
@@ -537,9 +537,14 @@ def agent():
|
|
|
537
537
|
llm_enabled = preview_whatap_conf("llm_enabled")
|
|
538
538
|
|
|
539
539
|
if apm_enabled == 'true':
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
540
|
+
if sys.platform.startswith('linux'):
|
|
541
|
+
# Publish native ownership before a short-lived interpreter
|
|
542
|
+
# can exit. This does not wait for collector/TCP readiness.
|
|
543
|
+
go()
|
|
544
|
+
else:
|
|
545
|
+
t = threading.Thread(target=go)
|
|
546
|
+
t.setDaemon(True)
|
|
547
|
+
t.start()
|
|
543
548
|
|
|
544
549
|
if llm_enabled == 'true':
|
|
545
550
|
# llm_enabled=true 라도 LLM 채널 자격증명(llm_accesskey)이 없으면 LLM
|
|
@@ -571,7 +576,60 @@ AGENT_NAME = 'whatap_python'
|
|
|
571
576
|
|
|
572
577
|
|
|
573
578
|
|
|
579
|
+
def _native_agent_spec(opts):
|
|
580
|
+
from whatap.util.native_process import AgentSpec
|
|
581
|
+
source = os.path.join(os.path.dirname(__file__), 'agent', 'linux',
|
|
582
|
+
ARCH[platform.machine()], AGENT_NAME)
|
|
583
|
+
env = os.environ.copy()
|
|
584
|
+
env.update(opts)
|
|
585
|
+
return AgentSpec(os.environ['WHATAP_HOME'], source, env)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _native_agent_log_path():
|
|
589
|
+
return os.path.join(os.environ.get('WHATAP_HOME', '.'), 'logs', AGENT_NAME + '.log')
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _open_native_agent_log():
|
|
593
|
+
# managed 경로는 PIPE 를 읽지 않으므로 네이티브 stdout/stderr 를 파일로 남겨
|
|
594
|
+
# 기동 직후 실패 원인을 추적할 수 있게 한다.
|
|
595
|
+
try:
|
|
596
|
+
log_path = _native_agent_log_path()
|
|
597
|
+
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
|
598
|
+
return open(log_path, 'ab')
|
|
599
|
+
except Exception as e:
|
|
600
|
+
whatap_print('WHATAP: native agent log open failed: %s' % e)
|
|
601
|
+
return subprocess.DEVNULL
|
|
602
|
+
|
|
603
|
+
|
|
574
604
|
def go(batch=False, opts={}, llm=False):
|
|
605
|
+
if sys.platform.startswith('linux') and not batch and not llm:
|
|
606
|
+
from whatap.util.native_process import FAILED, ensure_agent_running
|
|
607
|
+
|
|
608
|
+
def on_child_exit(pid, exit_code):
|
|
609
|
+
whatap_error_log(
|
|
610
|
+
'WHATAP: native APM exited (PID: {}, exit code: {}, log: {})'.format(
|
|
611
|
+
pid, exit_code, _native_agent_log_path()),
|
|
612
|
+
log_id='WHATAP_AGENT_EXITED')
|
|
613
|
+
|
|
614
|
+
try:
|
|
615
|
+
spec = _native_agent_spec(opts)
|
|
616
|
+
result = ensure_agent_running(
|
|
617
|
+
spec, lambda: _start_go(batch, opts, llm, managed=True),
|
|
618
|
+
on_child_exit=on_child_exit)
|
|
619
|
+
if result.status == FAILED:
|
|
620
|
+
whatap_error_log('WHATAP: native APM startup failed',
|
|
621
|
+
log_id='WHATAP_AGENT_START_FAILED')
|
|
622
|
+
else:
|
|
623
|
+
whatap_print('WHATAP: native APM {} (PID: {})'.format(
|
|
624
|
+
result.status, result.pid))
|
|
625
|
+
except Exception as e:
|
|
626
|
+
whatap_error_log('WHATAP: native APM startup failed: {}'.format(e),
|
|
627
|
+
log_id='WHATAP_AGENT_START_FAILED')
|
|
628
|
+
return
|
|
629
|
+
return _start_go(batch, opts, llm)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _start_go(batch=False, opts={}, llm=False, managed=False):
|
|
575
633
|
# LLM 에이전트는 Linux/macOS 를 지원한다(전용 whatap_python 이
|
|
576
634
|
# $WHATAP_HOME/<os>/<arch> 에 배포됨 — linux/darwin × amd64/arm64).
|
|
577
635
|
# 그 외 OS(Windows 등)는 전용 바이너리가 없으므로 우회적 "binary not found"
|
|
@@ -625,7 +683,7 @@ def go(batch=False, opts={}, llm=False):
|
|
|
625
683
|
return ''
|
|
626
684
|
|
|
627
685
|
pid = read_file(home, file_name)
|
|
628
|
-
if pid and get_pname(pid).find('whatap_python') >= 0:
|
|
686
|
+
if not managed and pid and get_pname(pid).find('whatap_python') >= 0:
|
|
629
687
|
if sys.platform == 'win32':
|
|
630
688
|
# Windows: Don't kill, just skip
|
|
631
689
|
whatap_print('WHATAP: Agent already running (PID: {}). Skipping duplicate execution.'.format(pid))
|
|
@@ -774,6 +832,17 @@ def go(batch=False, opts={}, llm=False):
|
|
|
774
832
|
cwd=home_path, env=newenv,
|
|
775
833
|
creationflags=DETACHED_PROCESS,
|
|
776
834
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
835
|
+
elif managed:
|
|
836
|
+
go_out = _open_native_agent_log()
|
|
837
|
+
try:
|
|
838
|
+
process = subprocess.Popen(cmd_args,
|
|
839
|
+
cwd=home_path, env=newenv,
|
|
840
|
+
start_new_session=True,
|
|
841
|
+
stdout=go_out, stderr=go_out)
|
|
842
|
+
finally:
|
|
843
|
+
if go_out is not subprocess.DEVNULL:
|
|
844
|
+
go_out.close()
|
|
845
|
+
return process
|
|
777
846
|
else:
|
|
778
847
|
process = subprocess.Popen(cmd_args,
|
|
779
848
|
cwd=home_path, env=newenv,
|
|
@@ -1086,6 +1155,22 @@ def get_port_number(port=6600, home=os.environ.get('WHATAP_HOME'), lock_file=Non
|
|
|
1086
1155
|
|
|
1087
1156
|
|
|
1088
1157
|
def configPort():
|
|
1158
|
+
if sys.platform.startswith('linux') and preview_whatap_conf('apm_enabled') == 'true':
|
|
1159
|
+
from whatap.util.native_process import acquire_start_lock, configured_channel, find_existing_agent
|
|
1160
|
+
try:
|
|
1161
|
+
spec = _native_agent_spec({})
|
|
1162
|
+
with acquire_start_lock(spec):
|
|
1163
|
+
if find_existing_agent(spec):
|
|
1164
|
+
# CLI allocation must not rewrite a live bootstrap agent's channel.
|
|
1165
|
+
return configured_channel(spec.home, spec.environment, include_unix=False)[1]
|
|
1166
|
+
return _config_port()
|
|
1167
|
+
except Exception as e:
|
|
1168
|
+
# 락 타임아웃/다중 인스턴스 등은 CLI 를 죽이지 말고 기존 포트 할당으로 폴백
|
|
1169
|
+
whatap_print('WHATAP: live agent check skipped, falling back to port file: %s' % e)
|
|
1170
|
+
return _config_port()
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def _config_port():
|
|
1089
1174
|
# force_net_udp_port가 설정된 경우 lock 파일 무시하고 해당 포트 강제 사용
|
|
1090
1175
|
force_port_str = preview_whatap_conf("force_net_udp_port")
|
|
1091
1176
|
if force_port_str and force_port_str not in ('false', 'False'):
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"""Linux APM native process ownership (not instrumentation configuration)."""
|
|
2
|
+
import os
|
|
3
|
+
import errno
|
|
4
|
+
import fcntl
|
|
5
|
+
import filecmp
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import stat
|
|
9
|
+
import subprocess
|
|
10
|
+
import tempfile
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from collections import namedtuple
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
STARTED = 'started'
|
|
18
|
+
REUSED = 'reused'
|
|
19
|
+
FAILED = 'failed'
|
|
20
|
+
IN_PROGRESS = 'in_progress'
|
|
21
|
+
StartResult = namedtuple('StartResult', 'status pid')
|
|
22
|
+
DISCOVERY_TIMEOUT_SECONDS = 1.0
|
|
23
|
+
LOCK_TIMEOUT_SECONDS = 1.0
|
|
24
|
+
LOCK_RETRY_SECONDS = 0.01
|
|
25
|
+
DEFAULT_UDP_PORT = 6600
|
|
26
|
+
CONFIG_MAX_BYTES = 1024 * 1024
|
|
27
|
+
RECORD_MAX_BYTES = 8192
|
|
28
|
+
CHILD_STOP_TIMEOUT_SECONDS = 2.0
|
|
29
|
+
STARTUP_GRACE_SECONDS = 0.5
|
|
30
|
+
STARTUP_POLL_SECONDS = 0.05
|
|
31
|
+
_TRUE_VALUES = ('1', 't', 'T', 'true', 'TRUE', 'True')
|
|
32
|
+
_watchers = set()
|
|
33
|
+
_watchers_lock = threading.Lock()
|
|
34
|
+
_start_lock_fds = set()
|
|
35
|
+
_start_lock_fds_lock = threading.Lock()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _before_fork():
|
|
39
|
+
# Serialize only descriptor bookkeeping, never flock acquisition or yield.
|
|
40
|
+
_start_lock_fds_lock.acquire()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _after_fork_parent():
|
|
44
|
+
_start_lock_fds_lock.release()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _after_fork():
|
|
48
|
+
global _watchers, _watchers_lock, _start_lock_fds, _start_lock_fds_lock
|
|
49
|
+
for fd in _start_lock_fds:
|
|
50
|
+
# LOCK_UN would also unlock the parent's shared open-file description.
|
|
51
|
+
try:
|
|
52
|
+
os.close(fd)
|
|
53
|
+
except OSError:
|
|
54
|
+
pass
|
|
55
|
+
_start_lock_fds = set()
|
|
56
|
+
_start_lock_fds_lock = threading.Lock()
|
|
57
|
+
_watchers = set()
|
|
58
|
+
_watchers_lock = threading.Lock()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
os.register_at_fork(before=_before_fork, after_in_parent=_after_fork_parent,
|
|
62
|
+
after_in_child=_after_fork)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def watch_owned_child(pid, process=None, on_exit=None):
|
|
66
|
+
if process is None:
|
|
67
|
+
try:
|
|
68
|
+
fields = (Path('/proc') / str(pid) / 'stat').read_text().rsplit(')', 1)[1].split()
|
|
69
|
+
if int(fields[1]) != os.getpid():
|
|
70
|
+
return
|
|
71
|
+
except (FileNotFoundError, ProcessLookupError):
|
|
72
|
+
return
|
|
73
|
+
key = (os.getpid(), pid)
|
|
74
|
+
with _watchers_lock:
|
|
75
|
+
if key in _watchers:
|
|
76
|
+
return
|
|
77
|
+
_watchers.add(key)
|
|
78
|
+
|
|
79
|
+
def wait():
|
|
80
|
+
try:
|
|
81
|
+
if process is not None:
|
|
82
|
+
process.wait()
|
|
83
|
+
else:
|
|
84
|
+
os.waitpid(pid, 0)
|
|
85
|
+
except ChildProcessError:
|
|
86
|
+
# An application may also reap its children. Never wait for -1 or
|
|
87
|
+
# replace SIGCHLD: those are application-owned contracts.
|
|
88
|
+
pass
|
|
89
|
+
finally:
|
|
90
|
+
if on_exit is not None:
|
|
91
|
+
try:
|
|
92
|
+
on_exit()
|
|
93
|
+
except (OSError, RuntimeError):
|
|
94
|
+
# A stale record is safe: it is revalidated on every start.
|
|
95
|
+
pass
|
|
96
|
+
with _watchers_lock:
|
|
97
|
+
_watchers.discard(key)
|
|
98
|
+
|
|
99
|
+
thread = threading.Thread(target=wait, name='whatap-native-wait-{}'.format(pid))
|
|
100
|
+
thread.daemon = True
|
|
101
|
+
try:
|
|
102
|
+
thread.start()
|
|
103
|
+
except Exception:
|
|
104
|
+
with _watchers_lock:
|
|
105
|
+
_watchers.discard(key)
|
|
106
|
+
raise
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def configured_channel(home, env, include_unix=True):
|
|
110
|
+
"""Read only native transport keys, including native file/ENV precedence.
|
|
111
|
+
|
|
112
|
+
whatap.port is an object identity input, NOT the UDP listener port.
|
|
113
|
+
"""
|
|
114
|
+
aliases = {'net_udp_port': 'WHATAP_NET_UDP_PORT',
|
|
115
|
+
'use_env_first': 'WHATAP_USE_ENV_FIRST'}
|
|
116
|
+
keys = ('net_udp_port', 'use_env_first', 'unix_socket_enabled', 'unix_socket')
|
|
117
|
+
config_home = env.get('WHATAP_CONFIG_HOME') or home
|
|
118
|
+
config_name = env.get('WHATAP_CONFIG') or 'whatap.conf'
|
|
119
|
+
# Preserve Go path.Join semantics before resolving against native cwd.
|
|
120
|
+
config_path = os.path.normpath(config_home + '/' + config_name)
|
|
121
|
+
if not os.path.isabs(config_path):
|
|
122
|
+
config_path = os.path.join(home, config_path)
|
|
123
|
+
values = {}
|
|
124
|
+
try:
|
|
125
|
+
with open(config_path) as stream:
|
|
126
|
+
content = stream.read(CONFIG_MAX_BYTES + 1)
|
|
127
|
+
if len(content) > CONFIG_MAX_BYTES:
|
|
128
|
+
raise RuntimeError('native configuration exceeds startup read limit')
|
|
129
|
+
for line in content.splitlines():
|
|
130
|
+
line = line.strip()
|
|
131
|
+
if not line or line.startswith(('#', '!')):
|
|
132
|
+
continue
|
|
133
|
+
key, separator, value = line.partition('=')
|
|
134
|
+
if separator and key.strip() in keys:
|
|
135
|
+
values[key.strip()] = value.strip()
|
|
136
|
+
except FileNotFoundError:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
def env_value(key):
|
|
140
|
+
return (env.get(key) or env.get(aliases.get(key, ''), '')).strip()
|
|
141
|
+
|
|
142
|
+
# The native loader resets UseEnvFirst before reading UseEnvFirst itself.
|
|
143
|
+
env_first = values.get('use_env_first', env_value('use_env_first')) in _TRUE_VALUES
|
|
144
|
+
|
|
145
|
+
def get(key, default):
|
|
146
|
+
value = env_value(key)
|
|
147
|
+
if not (env_first and value):
|
|
148
|
+
value = values.get(key, value)
|
|
149
|
+
return value or default
|
|
150
|
+
|
|
151
|
+
if include_unix and get('unix_socket_enabled', 'false') in _TRUE_VALUES:
|
|
152
|
+
# Go path.Join joins even a leading slash in its second argument.
|
|
153
|
+
path = os.path.normpath(home + '/' + get('unix_socket', 'whatap.sock'))
|
|
154
|
+
return ('unix', path)
|
|
155
|
+
try:
|
|
156
|
+
value = get('net_udp_port', str(DEFAULT_UDP_PORT))
|
|
157
|
+
# Native strconv.ParseInt(value, 10, 32) rejects Python-only syntax.
|
|
158
|
+
if re.fullmatch(r'[+-]?[0-9]+', value) is None:
|
|
159
|
+
raise ValueError('invalid native decimal')
|
|
160
|
+
port = int(value)
|
|
161
|
+
if not -2147483648 <= port <= 2147483647:
|
|
162
|
+
raise ValueError('native decimal exceeds int32')
|
|
163
|
+
except ValueError:
|
|
164
|
+
port = DEFAULT_UDP_PORT
|
|
165
|
+
if not 0 <= port <= 65535:
|
|
166
|
+
raise ValueError('native APM UDP port is out of range')
|
|
167
|
+
return ('udp', port)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def channel_socket_inodes(channel):
|
|
171
|
+
kind, address = channel
|
|
172
|
+
inodes = set()
|
|
173
|
+
tables = ('unix',) if kind == 'unix' else ('udp', 'udp6')
|
|
174
|
+
for table in tables:
|
|
175
|
+
try:
|
|
176
|
+
lines = (Path('/proc/net') / table).read_text().splitlines()[1:]
|
|
177
|
+
except FileNotFoundError:
|
|
178
|
+
if table == 'udp6':
|
|
179
|
+
continue
|
|
180
|
+
raise
|
|
181
|
+
for line in lines:
|
|
182
|
+
fields = line.split(None, 7) if kind == 'unix' else line.split()
|
|
183
|
+
if kind == 'unix':
|
|
184
|
+
if len(fields) == 8 and fields[7] == address:
|
|
185
|
+
inodes.add(fields[6])
|
|
186
|
+
elif address and int(fields[1].rsplit(':', 1)[1], 16) == address:
|
|
187
|
+
inodes.add(fields[9])
|
|
188
|
+
return inodes
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def owns_socket(pid, inodes):
|
|
192
|
+
sockets = {'socket:[{}]'.format(inode) for inode in inodes}
|
|
193
|
+
if not sockets:
|
|
194
|
+
return False
|
|
195
|
+
try:
|
|
196
|
+
with os.scandir('/proc/{}/fd'.format(pid)) as descriptors:
|
|
197
|
+
for descriptor in descriptors:
|
|
198
|
+
try:
|
|
199
|
+
sockets.discard(os.readlink(descriptor.path))
|
|
200
|
+
if not sockets:
|
|
201
|
+
return True
|
|
202
|
+
except FileNotFoundError:
|
|
203
|
+
continue
|
|
204
|
+
except FileNotFoundError:
|
|
205
|
+
return False
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class AgentSpec(object):
|
|
210
|
+
def __init__(self, home, executable, env=None):
|
|
211
|
+
self.home = os.path.realpath(home)
|
|
212
|
+
self.executable = os.path.realpath(executable)
|
|
213
|
+
environment = os.environ if env is None else env
|
|
214
|
+
# Retain only transport/config-location keys, never credentials.
|
|
215
|
+
self.environment = {key: environment[key] for key in (
|
|
216
|
+
'net_udp_port', 'WHATAP_NET_UDP_PORT', 'use_env_first',
|
|
217
|
+
'WHATAP_USE_ENV_FIRST', 'unix_socket_enabled', 'unix_socket',
|
|
218
|
+
'WHATAP_CONFIG_HOME', 'WHATAP_CONFIG') if key in environment}
|
|
219
|
+
self.channel = None # Read under the startup lock, not before waiting.
|
|
220
|
+
self.pid_file = Path(self.home) / 'whatap_python.pid'
|
|
221
|
+
namespaces = '{}-{}'.format(os.stat('/proc/self/ns/pid').st_ino,
|
|
222
|
+
os.stat('/proc/self/ns/net').st_ino)
|
|
223
|
+
self.namespaces = namespaces
|
|
224
|
+
self.lock_file = Path(self.home) / ('whatap_python.' + namespaces + '.lock')
|
|
225
|
+
self.record_file = Path(self.home) / ('whatap_python.' + namespaces + '.runtime.json')
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _process_stat(pid):
|
|
229
|
+
# comm can contain spaces/parentheses. After its final ')', state is field 3
|
|
230
|
+
# and the process start time is field 22.
|
|
231
|
+
fields = (Path('/proc') / str(pid) / 'stat').read_text().rsplit(')', 1)[1].split()
|
|
232
|
+
return {'pid': pid, 'state': fields[0], 'ppid': int(fields[1]), 'start_time': fields[19]}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _read_record(spec):
|
|
236
|
+
try:
|
|
237
|
+
with spec.record_file.open() as stream:
|
|
238
|
+
content = stream.read(RECORD_MAX_BYTES + 1)
|
|
239
|
+
if len(content) > RECORD_MAX_BYTES:
|
|
240
|
+
raise RuntimeError('native APM ownership record is too large')
|
|
241
|
+
value = json.loads(content)
|
|
242
|
+
return value if isinstance(value, dict) else {}
|
|
243
|
+
except (FileNotFoundError, ValueError):
|
|
244
|
+
return {}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _atomic_write(path, content):
|
|
248
|
+
fd, temporary = tempfile.mkstemp(prefix=path.name + '.', dir=str(path.parent))
|
|
249
|
+
try:
|
|
250
|
+
with os.fdopen(fd, 'w') as stream:
|
|
251
|
+
stream.write(content)
|
|
252
|
+
os.replace(temporary, str(path))
|
|
253
|
+
finally:
|
|
254
|
+
try:
|
|
255
|
+
os.unlink(temporary)
|
|
256
|
+
except FileNotFoundError:
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def write_runtime_record(spec, pid):
|
|
261
|
+
info = _process_stat(pid)
|
|
262
|
+
if info['state'] in ('Z', 'X'):
|
|
263
|
+
raise RuntimeError('native APM exited during startup')
|
|
264
|
+
record = {'pid': pid, 'start_time': info['start_time'],
|
|
265
|
+
'channel': list(spec.channel), 'namespaces': spec.namespaces}
|
|
266
|
+
if _read_record(spec) != record:
|
|
267
|
+
_atomic_write(spec.record_file, json.dumps(record))
|
|
268
|
+
try:
|
|
269
|
+
unchanged = spec.pid_file.read_text().strip() == str(pid)
|
|
270
|
+
except FileNotFoundError:
|
|
271
|
+
unchanged = False
|
|
272
|
+
if not unchanged:
|
|
273
|
+
_atomic_write(spec.pid_file, str(pid))
|
|
274
|
+
return record
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _clear_record(spec, record):
|
|
278
|
+
with acquire_start_lock(spec):
|
|
279
|
+
if _read_record(spec) == record:
|
|
280
|
+
spec.record_file.unlink()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@contextmanager
|
|
284
|
+
def acquire_start_lock(spec):
|
|
285
|
+
flags = os.O_RDWR | os.O_CREAT | os.O_CLOEXEC | os.O_NOFOLLOW
|
|
286
|
+
owner_pid = os.getpid()
|
|
287
|
+
with _start_lock_fds_lock:
|
|
288
|
+
fd = os.open(str(spec.lock_file), flags, 0o600)
|
|
289
|
+
_start_lock_fds.add(fd)
|
|
290
|
+
try:
|
|
291
|
+
info = os.fstat(fd)
|
|
292
|
+
if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
|
|
293
|
+
raise RuntimeError('unsafe native APM lock file')
|
|
294
|
+
deadline = time.monotonic() + LOCK_TIMEOUT_SECONDS
|
|
295
|
+
while True:
|
|
296
|
+
try:
|
|
297
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
298
|
+
break
|
|
299
|
+
except OSError as error:
|
|
300
|
+
if error.errno not in (errno.EAGAIN, errno.EACCES):
|
|
301
|
+
raise
|
|
302
|
+
if time.monotonic() >= deadline:
|
|
303
|
+
raise TimeoutError('native APM startup lock is busy')
|
|
304
|
+
time.sleep(LOCK_RETRY_SECONDS)
|
|
305
|
+
yield
|
|
306
|
+
finally:
|
|
307
|
+
# Closing releases the lock. Never unlink: another waiter may have opened
|
|
308
|
+
# this inode already, and a replacement inode would allow two owners.
|
|
309
|
+
if os.getpid() == owner_pid:
|
|
310
|
+
with _start_lock_fds_lock:
|
|
311
|
+
try:
|
|
312
|
+
os.close(fd)
|
|
313
|
+
finally:
|
|
314
|
+
_start_lock_fds.discard(fd)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _matching_process(pid, spec, record):
|
|
318
|
+
"""A name/PID alone is not an identity; do not adopt other applications."""
|
|
319
|
+
root = Path('/proc') / str(pid)
|
|
320
|
+
try:
|
|
321
|
+
info = _process_stat(pid)
|
|
322
|
+
if info['state'] in ('Z', 'X'):
|
|
323
|
+
return False
|
|
324
|
+
if os.path.realpath(os.readlink(str(root / 'cwd'))) != spec.home:
|
|
325
|
+
return False
|
|
326
|
+
args = (root / 'cmdline').read_bytes().split(b'\0')
|
|
327
|
+
if b'--llm' in args:
|
|
328
|
+
return False
|
|
329
|
+
namespaces = '{}-{}'.format(os.stat(str(root / 'ns/pid')).st_ino,
|
|
330
|
+
os.stat(str(root / 'ns/net')).st_ino)
|
|
331
|
+
if namespaces != spec.namespaces:
|
|
332
|
+
return False
|
|
333
|
+
process_exe = str(root / 'exe')
|
|
334
|
+
same_binary = os.path.samefile(process_exe, spec.executable)
|
|
335
|
+
if not same_binary and os.path.realpath(os.readlink(process_exe)) == os.path.join(spec.home, 'whatap_python'):
|
|
336
|
+
# _start_go copies when the filesystem does not support symlinks.
|
|
337
|
+
# Path/name alone is insufficient: compare the actual executable.
|
|
338
|
+
same_binary = filecmp.cmp(process_exe, spec.executable, shallow=False)
|
|
339
|
+
if not same_binary:
|
|
340
|
+
if (root / 'comm').read_text().strip() == 'whatap_python':
|
|
341
|
+
raise RuntimeError('different native APM executable is already running in this home')
|
|
342
|
+
return False
|
|
343
|
+
if record.get('pid') == pid and record.get('start_time') == info['start_time']:
|
|
344
|
+
if record.get('channel') != list(spec.channel):
|
|
345
|
+
raise RuntimeError('running native APM channel differs from current configuration')
|
|
346
|
+
return True
|
|
347
|
+
except (FileNotFoundError, ProcessLookupError):
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def find_existing_agent(spec):
|
|
352
|
+
"""Caller holds acquire_start_lock, including during port-file allocation."""
|
|
353
|
+
spec.channel = configured_channel(spec.home, spec.environment)
|
|
354
|
+
record = _read_record(spec)
|
|
355
|
+
recorded_pid = record.get('pid')
|
|
356
|
+
if isinstance(recorded_pid, int) and recorded_pid > 0:
|
|
357
|
+
if _matching_process(recorded_pid, spec, record):
|
|
358
|
+
return recorded_pid
|
|
359
|
+
try:
|
|
360
|
+
pid = int(spec.pid_file.read_text().strip())
|
|
361
|
+
except (FileNotFoundError, ValueError):
|
|
362
|
+
pid = None
|
|
363
|
+
if pid and pid > 0 and _matching_process(pid, spec, record):
|
|
364
|
+
return pid
|
|
365
|
+
matches = []
|
|
366
|
+
deadline = time.monotonic() + DISCOVERY_TIMEOUT_SECONDS
|
|
367
|
+
with os.scandir('/proc') as entries:
|
|
368
|
+
for entry in entries:
|
|
369
|
+
if time.monotonic() > deadline:
|
|
370
|
+
raise RuntimeError('native APM process discovery timed out')
|
|
371
|
+
if not entry.name.isdigit():
|
|
372
|
+
continue
|
|
373
|
+
try:
|
|
374
|
+
comm = (Path(entry.path) / 'comm').read_text().strip()
|
|
375
|
+
if comm != 'whatap_python':
|
|
376
|
+
continue
|
|
377
|
+
candidate = int(entry.name)
|
|
378
|
+
if _matching_process(candidate, spec, record):
|
|
379
|
+
matches.append(candidate)
|
|
380
|
+
except (FileNotFoundError, ProcessLookupError):
|
|
381
|
+
continue
|
|
382
|
+
if len(matches) > 1:
|
|
383
|
+
raise RuntimeError('multiple native APM instances already exist')
|
|
384
|
+
return matches[0] if matches else None
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def ensure_agent_running(spec, spawn, on_child_exit=None):
|
|
388
|
+
try:
|
|
389
|
+
with acquire_start_lock(spec):
|
|
390
|
+
return _ensure_locked(spec, spawn, on_child_exit)
|
|
391
|
+
except TimeoutError:
|
|
392
|
+
return StartResult(IN_PROGRESS, None)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _early_exit_code(process):
|
|
396
|
+
"""Return the exit code if the child dies within the startup grace period."""
|
|
397
|
+
deadline = time.monotonic() + STARTUP_GRACE_SECONDS
|
|
398
|
+
while True:
|
|
399
|
+
code = process.poll()
|
|
400
|
+
if isinstance(code, int):
|
|
401
|
+
return code
|
|
402
|
+
if time.monotonic() >= deadline:
|
|
403
|
+
return None
|
|
404
|
+
time.sleep(STARTUP_POLL_SECONDS)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _owned_child_exited(spec, record, process, on_child_exit):
|
|
408
|
+
try:
|
|
409
|
+
_clear_record(spec, record)
|
|
410
|
+
finally:
|
|
411
|
+
if on_child_exit is not None:
|
|
412
|
+
try:
|
|
413
|
+
on_child_exit(process.pid, process.returncode)
|
|
414
|
+
except Exception:
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _ensure_locked(spec, spawn, on_child_exit=None):
|
|
419
|
+
pid = find_existing_agent(spec)
|
|
420
|
+
occupied = channel_socket_inodes(spec.channel)
|
|
421
|
+
if occupied and (not pid or not owns_socket(pid, occupied)):
|
|
422
|
+
raise RuntimeError('native APM channel is occupied by another process')
|
|
423
|
+
if pid:
|
|
424
|
+
previous = _read_record(spec)
|
|
425
|
+
known = (previous.get('pid') == pid and
|
|
426
|
+
previous.get('start_time') == _process_stat(pid)['start_time'] and
|
|
427
|
+
previous.get('channel') == list(spec.channel) and
|
|
428
|
+
previous.get('namespaces') == spec.namespaces)
|
|
429
|
+
if not known and not occupied:
|
|
430
|
+
# Legacy native may still be connecting, or use a different channel.
|
|
431
|
+
# Neither duplicate it nor silently certify its current config.
|
|
432
|
+
watch_owned_child(pid)
|
|
433
|
+
return StartResult(IN_PROGRESS, pid)
|
|
434
|
+
record = write_runtime_record(spec, pid)
|
|
435
|
+
watch_owned_child(pid, on_exit=lambda: _clear_record(spec, record))
|
|
436
|
+
return StartResult(REUSED, pid)
|
|
437
|
+
process = spawn()
|
|
438
|
+
if process is None:
|
|
439
|
+
return StartResult(FAILED, None)
|
|
440
|
+
exit_code = _early_exit_code(process)
|
|
441
|
+
if exit_code is not None:
|
|
442
|
+
raise RuntimeError('native APM exited immediately (PID {}, exit code {})'.format(
|
|
443
|
+
process.pid, exit_code))
|
|
444
|
+
try:
|
|
445
|
+
record = write_runtime_record(spec, process.pid)
|
|
446
|
+
watch_owned_child(process.pid, process,
|
|
447
|
+
on_exit=lambda: _owned_child_exited(spec, record, process, on_child_exit))
|
|
448
|
+
except Exception:
|
|
449
|
+
# Only this Popen's child is ours to stop. Never abandon a child after a
|
|
450
|
+
# metadata/thread failure, and never kill a process found by discovery.
|
|
451
|
+
process.terminate()
|
|
452
|
+
try:
|
|
453
|
+
process.wait(timeout=CHILD_STOP_TIMEOUT_SECONDS)
|
|
454
|
+
except subprocess.TimeoutExpired:
|
|
455
|
+
process.kill()
|
|
456
|
+
process.wait(timeout=CHILD_STOP_TIMEOUT_SECONDS)
|
|
457
|
+
raise
|
|
458
|
+
return StartResult(STARTED, process.pid)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|