scripts: dev_cli: Add signal handling for graceful cleanup

During development, it is common to cancel a running test and
rerun it after making changes. However, pressing Ctrl+C while
dev_cli.sh runs long-running container commands (wget, qemu-img,
cargo build, etc.) does not reliably terminate the process.
Bash defers signal handling while a foreground process is
running, so the trap only fires after the docker run command
returns. This makes it difficult to cancel and restart quickly.

Fix this by introducing a run_container() wrapper that runs
docker in the background and uses 'wait', which is immediately
interruptible by signals. A cleanup() trap handler is set for
SIGINT and SIGTERM that kills the named container, the tracked
background PID, and any remaining child processes.

Each docker run invocation is assigned a unique --name based
on the script PID (clh-dev-$$) to allow targeted cleanup.
The interactive shell (cmd_shell) is left unwrapped since it
needs foreground terminal I/O.

Signed-off-by: Muminul Islam <muislam@microsoft.com>
This commit is contained in:
Muminul Islam
2026-04-17 12:39:52 -07:00
committed by Rob Bradford
parent ae646f9220
commit 305451cce4

View File

@@ -53,6 +53,46 @@ CARGO_TARGET_DIR="${CLH_BUILD_DIR}/cargo_target"
# Let tests know that the special environment is set up.
RUSTFLAGS="${RUSTFLAGS} --cfg devcli_testenv"
# Container name used for cleanup on signal. The PID makes it unique per
# invocation so parallel runs do not collide.
CLH_CTR_NAME="clh-dev-$$"
# PID of the docker run process launched by run_container().
CLH_CTR_PID=""
# Cleanup handler: kill the running container (if any) and all child
# processes, then exit.
cleanup() {
echo "[$CLI_NAME] Caught signal, terminating..."
# Disable the trap to prevent recursion
trap - INT TERM
# Kill the Docker/Podman container by name
$DOCKER_RUNTIME kill "$CLH_CTR_NAME" 2>/dev/null
$DOCKER_RUNTIME kill "${CLH_CTR_NAME}-fix" 2>/dev/null
# Kill the docker run process tracked by run_container()
[ -n "$CLH_CTR_PID" ] && kill -TERM "$CLH_CTR_PID" 2>/dev/null
# Kill any remaining child processes
pkill -TERM -P $$ 2>/dev/null
wait 2>/dev/null
exit 1
}
trap cleanup INT TERM
# Run a command in the background and wait for it. Bash defers trap
# handling while a foreground process is running, which makes Ctrl+C
# unresponsive during long-running container commands (wget, qemu-img,
# cargo build, etc). By backgrounding the command and using `wait`,
# the trap fires immediately when a signal arrives.
run_container() {
"$@" &
CLH_CTR_PID=$!
wait $CLH_CTR_PID
local rc=$?
CLH_CTR_PID=""
return $rc
}
# Send a decorated message to stdout, followed by a new line
#
say() {
@@ -146,6 +186,7 @@ fix_dir_perms() {
# Yes, running Docker to get elevated privileges, just to chown some files
# is a dirty hack.
$DOCKER_RUNTIME run \
--name "${CLH_CTR_NAME}-fix" \
--workdir "$CTR_CLH_ROOT_DIR" \
--rm \
--volume /dev:/dev \
@@ -314,7 +355,8 @@ cmd_build() {
rustflags="$rustflags -C link-args=-Wl,-Bstatic -C link-args=-lc"
fi
$DOCKER_RUNTIME run \
run_container "$DOCKER_RUNTIME" run \
--name "$CLH_CTR_NAME" \
--user "$(id -u):$(id -g)" \
--workdir "$CTR_CLH_ROOT_DIR" \
--rm \
@@ -335,7 +377,8 @@ cmd_clean() {
ensure_build_dir
ensure_latest_ctr
$DOCKER_RUNTIME run \
run_container "$DOCKER_RUNTIME" run \
--name "$CLH_CTR_NAME" \
--user "$(id -u):$(id -g)" \
--workdir "$CTR_CLH_ROOT_DIR" \
--rm \
@@ -432,7 +475,8 @@ cmd_tests() {
if [[ "$unit" = true ]]; then
say "Running unit tests for $target..."
$DOCKER_RUNTIME run \
run_container "$DOCKER_RUNTIME" run \
--name "$CLH_CTR_NAME" \
--workdir "$CTR_CLH_ROOT_DIR" \
--rm \
--device $exported_device \
@@ -451,7 +495,8 @@ cmd_tests() {
if [ "$integration" = true ]; then
say "Running integration tests for $target..."
$DOCKER_RUNTIME run \
run_container "$DOCKER_RUNTIME" run \
--name "$CLH_CTR_NAME" \
--workdir "$CTR_CLH_ROOT_DIR" \
--rm \
--privileged \
@@ -477,7 +522,8 @@ cmd_tests() {
mkdir -p "$DEST_IGVM_FILES_PATH"
copy_igvm_files "$SRC_IGVM_FILES_PATH" "$DEST_IGVM_FILES_PATH"
say "Running CVM integration tests for $target..."
$DOCKER_RUNTIME run \
run_container "$DOCKER_RUNTIME" run \
--name "$CLH_CTR_NAME" \
--workdir "$CTR_CLH_ROOT_DIR" \
--rm \
--privileged \
@@ -502,7 +548,8 @@ cmd_tests() {
if [ "$integration_vfio" = true ]; then
say "Running VFIO integration tests for $target..."
$DOCKER_RUNTIME run \
run_container "$DOCKER_RUNTIME" run \
--name "$CLH_CTR_NAME" \
--workdir "$CTR_CLH_ROOT_DIR" \
--rm \
--privileged \
@@ -525,7 +572,8 @@ cmd_tests() {
if [ "$integration_windows" = true ]; then
say "Running Windows integration tests for $target..."
$DOCKER_RUNTIME run \
run_container "$DOCKER_RUNTIME" run \
--name "$CLH_CTR_NAME" \
--workdir "$CTR_CLH_ROOT_DIR" \
--rm \
--privileged \
@@ -548,7 +596,8 @@ cmd_tests() {
if [ "$integration_live_migration" = true ]; then
say "Running 'live migration' integration tests for $target..."
$DOCKER_RUNTIME run \
run_container "$DOCKER_RUNTIME" run \
--name "$CLH_CTR_NAME" \
--workdir "$CTR_CLH_ROOT_DIR" \
--rm \
--privileged \
@@ -573,7 +622,8 @@ cmd_tests() {
if [ "$integration_rate_limiter" = true ]; then
say "Running 'rate limiter' integration tests for $target..."
$DOCKER_RUNTIME run \
run_container "$DOCKER_RUNTIME" run \
--name "$CLH_CTR_NAME" \
--workdir "$CTR_CLH_ROOT_DIR" \
--rm \
--privileged \
@@ -596,7 +646,8 @@ cmd_tests() {
if [ "$metrics" = true ]; then
say "Generating performance metrics for $target..."
$DOCKER_RUNTIME run \
run_container "$DOCKER_RUNTIME" run \
--name "$CLH_CTR_NAME" \
--workdir "$CTR_CLH_ROOT_DIR" \
--rm \
--privileged \
@@ -620,7 +671,8 @@ cmd_tests() {
if [ "$coverage" = true ]; then
say "Generating code coverage information for $target..."
$DOCKER_RUNTIME run \
run_container "$DOCKER_RUNTIME" run \
--name "$CLH_CTR_NAME" \
--workdir "$CTR_CLH_ROOT_DIR" \
--rm \
--privileged \
@@ -721,6 +773,7 @@ cmd_shell() {
fi
$DOCKER_RUNTIME run \
--name "$CLH_CTR_NAME" \
$tty_args \
--workdir "$CTR_CLH_ROOT_DIR" \
--rm \