From cf3205447b965cfde6630766eacb86f43db74793 Mon Sep 17 00:00:00 2001 From: delphix-devops-bot Date: Wed, 28 Oct 2026 18:26:25 +0000 Subject: [PATCH] DLPX-94668 a signal that stops stbtrace can interrupt its probe detach stbtrace runs until it is signalled, and bcc detaches the probes from an atexit callback. That callback runs once the interpreter has started shutting down and with signal delivery still enabled, so a signal can land inside the detach: the command still exits 0 with a complete set of datapoints, but Python writes the interrupted callback's traceback to stderr. The os_tests stbtrace tests run the command under `timeout --signal=SIGINT` and fail on any stderr from a command that exited 0, so they go red a few percent of the time. Ignore SIGINT as soon as the first one is taken. The detach then always runs to completion, and it stays where bcc puts it rather than being reimplemented here. Each handler disarms only the signal it handles, so nothing becomes harder to stop than it is today: BCCHelper's SIGTERM handler exits the same way and is exposed the same way, and takes the same guard for SIGTERM. The `except KeyboardInterrupt` that used to be the stop path goes with it. No KeyboardInterrupt is raised once SIGINT has a handler of its own, and leaving a second, dead stop path in the file only obscures which one runs. Co-Authored-By: Claude Opus 5 --- cmd/stbtrace.py | 24 ++++++++++++++++++++++-- lib/bcchelper.py | 5 ++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/cmd/stbtrace.py b/cmd/stbtrace.py index 19373f4..71c5170 100755 --- a/cmd/stbtrace.py +++ b/cmd/stbtrace.py @@ -10,6 +10,7 @@ import argparse import json import os +import signal import sys base_dir = os.path.dirname(__file__) + "/../" if not os.path.exists(base_dir + "lib/iterative_template.py"): @@ -129,16 +130,35 @@ node != 'title': template.addSingleton(node, data[node]) + +# +# bcc detaches the probes from an atexit callback, which runs once the +# interpreter has started shutting down and with signal delivery still +# enabled. An interrupt that lands inside the detach aborts it and prints +# the callback's traceback to stderr, even though the trace itself finished +# and its datapoints are complete. Ignore the interrupt as soon as the +# first one is taken, so the detach always runs to completion. +# +def ignore_interrupt(): + signal.signal(signal.SIGINT, signal.SIG_IGN) + + +def exit_on_interrupt(signum, frame): + ignore_interrupt() + exit(0) + + # Perform template substitutions and execute or output the resulting script script = template.render() if args.bcc: print(script, end='') exit(0) else: + signal.signal(signal.SIGINT, exit_on_interrupt) try: exec(script) - except KeyboardInterrupt: - exit(0) except Exception as e: print(e) exit(1) + finally: + ignore_interrupt() diff --git a/lib/bcchelper.py b/lib/bcchelper.py index f424fe4..bbfaf5a 100755 --- a/lib/bcchelper.py +++ b/lib/bcchelper.py @@ -133,9 +133,12 @@ def __init__(self, b, mode=DEFAULT_PRINT_MODE): # # Try to run atexit handlers if we are killed by a signal. BCC # registers handlers to detach kprobes, and if these aren't run the - # probes are leaked. + # probes are leaked. Ignore the signal before exiting, so that a + # later one cannot land inside the detach and leave its traceback + # on stderr. # def _handler(signum, frame): + signal.signal(signal.SIGTERM, signal.SIG_IGN) exit(1) signal.signal(signal.SIGTERM, _handler)