feat: remote tab in task-create dialog — multi-row GitHub repo picker (#1116)
This commit is contained in:
Executable
+321
@@ -0,0 +1,321 @@
|
||||
#!/usr/bin/env bash
|
||||
# kandev-kill — terminate exactly ONE kandev instance: the backend listening on
|
||||
# a given port, plus its agentctl child(ren) and its matching web/next server.
|
||||
#
|
||||
# This NEVER does a blanket `pkill kandev`. It resolves the backend PID from the
|
||||
# port, confirms the process is actually a kandev backend, computes the exact
|
||||
# set of PIDs it will signal, PRINTS that set, and only then (after confirmation)
|
||||
# sends SIGTERM, escalating to SIGKILL after a short grace period.
|
||||
#
|
||||
# Safety:
|
||||
# - Refuses to act unless you explicitly pass a port or a --pidfile.
|
||||
# - GUARDED PORTS (the well-known production ports the user's live instance
|
||||
# uses — see GUARDED_PORTS below) are protected: killing one requires an
|
||||
# explicit --force. The guard is applied to the RESOLVED backend port, so it
|
||||
# holds no matter HOW you reached it — by `<port>` OR via a `--pidfile` whose
|
||||
# process happens to listen on a guarded port. There is no bypass path.
|
||||
# - Prints exactly what will be killed and asks for confirmation, unless --yes
|
||||
# is given (for scripted teardown).
|
||||
# - Only signals a process group (kill -- -<pid>) when the target PID is
|
||||
# actually the group leader; otherwise it kills the PID + its discovered
|
||||
# descendants individually, never a group it doesn't own.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/kandev-kill <backend_port> [--yes] [--force]
|
||||
# scripts/kandev-kill --pidfile /tmp/kandev-dev-isolated-<port>.pid [--yes]
|
||||
#
|
||||
# Examples:
|
||||
# scripts/kandev-kill 48429 --yes
|
||||
# scripts/kandev-kill --pidfile /tmp/kandev-dev-isolated-48429.pid --yes
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# Ports we refuse to kill without --force (the user's live production instance).
|
||||
# Extend by adding entries — the guard runs on the RESOLVED port, so every entry
|
||||
# is protected through both the <port> and --pidfile entry paths.
|
||||
GUARDED_PORTS=(38429)
|
||||
GRACE_SECONDS=5
|
||||
|
||||
# True if $1 is one of the guarded ports.
|
||||
is_guarded_port() {
|
||||
local p="$1" g
|
||||
for g in "${GUARDED_PORTS[@]}"; do
|
||||
[[ "$p" == "$g" ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
PORT=""
|
||||
PIDFILE=""
|
||||
ASSUME_YES=0
|
||||
FORCE=0
|
||||
|
||||
usage() {
|
||||
sed -n '2,29p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit "${1:-2}"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--pidfile)
|
||||
PIDFILE="${2:?--pidfile requires a path}"
|
||||
shift 2
|
||||
;;
|
||||
--yes|-y)
|
||||
ASSUME_YES=1
|
||||
shift
|
||||
;;
|
||||
--force)
|
||||
FORCE=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage 0
|
||||
;;
|
||||
--*)
|
||||
echo "kandev-kill: unknown flag '$1'" >&2
|
||||
usage 2
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$PORT" ]]; then
|
||||
PORT="$1"
|
||||
else
|
||||
echo "kandev-kill: unexpected argument '$1'" >&2
|
||||
usage 2
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Resolve the backend PID listening on a TCP port.
|
||||
pid_for_port() {
|
||||
local port="$1" pid=""
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
pid="$(ss -tlnpH "sport = :$port" 2>/dev/null |
|
||||
grep -oE 'pid=[0-9]+' | head -1 | cut -d= -f2)"
|
||||
fi
|
||||
if [[ -z "$pid" ]] && command -v lsof >/dev/null 2>&1; then
|
||||
pid="$(lsof -nP -iTCP:"$port" -sTCP:LISTEN -t 2>/dev/null | head -1)"
|
||||
fi
|
||||
printf '%s\n' "$pid"
|
||||
}
|
||||
|
||||
# List every TCP port a PID is listening on, one per line.
|
||||
listening_ports_for_pid() {
|
||||
local pid="$1"
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -tlnpH 2>/dev/null | grep "pid=$pid," |
|
||||
grep -oE ':[0-9]+ ' | tr -d ': ' | sort -nu
|
||||
elif command -v lsof >/dev/null 2>&1; then
|
||||
lsof -nP -iTCP -sTCP:LISTEN -a -p "$pid" 2>/dev/null |
|
||||
awk 'NR>1 {n=split($9,a,":"); print a[n]}' | sort -nu
|
||||
fi
|
||||
}
|
||||
|
||||
# Confirm a PID is a kandev backend (exe basename or cmdline mentions kandev).
|
||||
is_kandev_backend() {
|
||||
local pid="$1" exe cmd
|
||||
[[ -d "/proc/$pid" ]] || return 1
|
||||
exe="$(readlink "/proc/$pid/exe" 2>/dev/null || true)"
|
||||
[[ "$(basename "$exe" 2>/dev/null)" == "kandev" ]] && return 0
|
||||
cmd="$(tr '\0' ' ' <"/proc/$pid/cmdline" 2>/dev/null || true)"
|
||||
case "$cmd" in
|
||||
*/kandev*|*bin/kandev*) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- Resolve the target backend PID + port ---
|
||||
BACKEND_PID=""
|
||||
if [[ -n "$PIDFILE" ]]; then
|
||||
[[ -f "$PIDFILE" ]] || { echo "kandev-kill: pidfile not found: $PIDFILE" >&2; exit 1; }
|
||||
BACKEND_PID="$(tr -d '[:space:]' <"$PIDFILE")"
|
||||
[[ "$BACKEND_PID" =~ ^[0-9]+$ ]] || { echo "kandev-kill: pidfile has no numeric PID: $PIDFILE" >&2; exit 1; }
|
||||
if [[ ! -d "/proc/$BACKEND_PID" ]]; then
|
||||
echo "kandev-kill: process $BACKEND_PID from pidfile is already gone; nothing to do." >&2
|
||||
rm -f "$PIDFILE" 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
elif [[ -n "$PORT" ]]; then
|
||||
[[ "$PORT" =~ ^[0-9]+$ ]] || { echo "kandev-kill: port must be numeric, got '$PORT'" >&2; exit 2; }
|
||||
BACKEND_PID="$(pid_for_port "$PORT")"
|
||||
[[ -n "$BACKEND_PID" ]] || { echo "kandev-kill: no listening process found on port $PORT" >&2; exit 1; }
|
||||
else
|
||||
echo "kandev-kill: refusing to run without an explicit <port> or --pidfile." >&2
|
||||
usage 2
|
||||
fi
|
||||
|
||||
# --- Resolve EVERY listening port of the target PID (for guard + display) ---
|
||||
# The guard must hold no matter how the target was supplied. We derive all
|
||||
# listening ports from the running process itself, so a --pidfile pointing at a
|
||||
# guarded port is refused exactly like `kandev-kill <guarded_port>`.
|
||||
mapfile -t TARGET_PORTS < <(listening_ports_for_pid "$BACKEND_PID")
|
||||
# Fail closed: in --pidfile mode the guard relies entirely on resolving the
|
||||
# target's listening ports. If neither ss nor lsof is available (empty list and
|
||||
# no explicit port supplied), we can't prove the target isn't a guarded
|
||||
# production instance — refuse rather than kill blind, unless --force.
|
||||
if [[ -n "$PIDFILE" && -z "$PORT" && ${#TARGET_PORTS[@]} -eq 0 && "$FORCE" -ne 1 ]]; then
|
||||
echo "kandev-kill: cannot resolve listening ports for PID $BACKEND_PID (need ss or lsof)." >&2
|
||||
echo " Refusing a --pidfile kill without --force — the guarded-port check can't run." >&2
|
||||
exit 3
|
||||
fi
|
||||
# Prefer the explicitly supplied port for display; else the lowest listening one.
|
||||
if [[ -z "$PORT" && ${#TARGET_PORTS[@]} -gt 0 ]]; then
|
||||
PORT="${TARGET_PORTS[0]}"
|
||||
fi
|
||||
|
||||
# --- Safety guards ---
|
||||
# Refuse if ANY resolved listening port (or the supplied port) is guarded.
|
||||
guarded_hit=""
|
||||
if [[ -n "${PORT:-}" ]] && is_guarded_port "$PORT"; then
|
||||
guarded_hit="$PORT"
|
||||
fi
|
||||
for p in "${TARGET_PORTS[@]:-}"; do
|
||||
[[ -z "$p" ]] && continue
|
||||
if is_guarded_port "$p"; then
|
||||
guarded_hit="$p"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -n "$guarded_hit" && "$FORCE" -ne 1 ]]; then
|
||||
echo "kandev-kill: port $guarded_hit is a guarded production port (the user's live instance)." >&2
|
||||
echo " PID $BACKEND_PID listens on it — refusing to kill without --force." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if ! is_kandev_backend "$BACKEND_PID"; then
|
||||
echo "kandev-kill: PID $BACKEND_PID (port ${PORT:-?}) does not look like a kandev backend. Aborting." >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
# --- Compute the kill set: backend + descendants (agentctl + web if children) ---
|
||||
# Collect the full descendant tree of the backend PID.
|
||||
collect_descendants() {
|
||||
local root="$1"
|
||||
local -a queue=("$root")
|
||||
local -a out=()
|
||||
while [[ ${#queue[@]} -gt 0 ]]; do
|
||||
local cur="${queue[0]}"
|
||||
queue=("${queue[@]:1}")
|
||||
out+=("$cur")
|
||||
local kids
|
||||
kids="$(pgrep -P "$cur" 2>/dev/null || true)"
|
||||
for k in $kids; do
|
||||
queue+=("$k")
|
||||
done
|
||||
done
|
||||
printf '%s\n' "${out[@]}"
|
||||
}
|
||||
|
||||
mapfile -t TREE < <(collect_descendants "$BACKEND_PID")
|
||||
|
||||
# The web/next server is typically a sibling (spawned by the launcher, not a
|
||||
# child of the backend). If a pidfile sidecar lists it, include it.
|
||||
WEB_PID=""
|
||||
if [[ -n "$PIDFILE" && -f "${PIDFILE%.pid}.web.pid" ]]; then
|
||||
WEB_PID="$(tr -d '[:space:]' <"${PIDFILE%.pid}.web.pid" 2>/dev/null || true)"
|
||||
if [[ "$WEB_PID" =~ ^[0-9]+$ && -d "/proc/$WEB_PID" ]]; then
|
||||
TREE+=("$WEB_PID")
|
||||
else
|
||||
WEB_PID=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Describe the kill set.
|
||||
echo "kandev-kill: will terminate the following processes (backend port ${PORT:-?}):"
|
||||
for p in "${TREE[@]}"; do
|
||||
cmd="$( (tr '\0' ' ' <"/proc/$p/cmdline") 2>/dev/null | cut -c1-90 || true)"
|
||||
echo " PID $p ${cmd:-<gone>}"
|
||||
done
|
||||
|
||||
if [[ "$ASSUME_YES" -ne 1 ]]; then
|
||||
printf 'Proceed? [y/N] '
|
||||
read -r answer
|
||||
case "$answer" in
|
||||
y|Y|yes|YES) ;;
|
||||
*) echo "kandev-kill: aborted by user."; exit 0 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Is the backend PID its own process-group leader? Only then is it safe to
|
||||
# signal the whole group (kill -- -<pid>); otherwise that negative PID would
|
||||
# target some OTHER process's group. `dev-isolated` launches via setsid (leader),
|
||||
# but a kandev started another way may not be.
|
||||
pid_is_pgleader() {
|
||||
local pid="$1" pgid
|
||||
pgid="$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d '[:space:]')"
|
||||
[[ -n "$pgid" && "$pgid" == "$pid" ]]
|
||||
}
|
||||
|
||||
BACKEND_IS_LEADER=0
|
||||
if pid_is_pgleader "$BACKEND_PID"; then
|
||||
BACKEND_IS_LEADER=1
|
||||
fi
|
||||
|
||||
# A PID is "alive" only if it exists AND is not a zombie. A zombie (state Z) has
|
||||
# already exited; it just hasn't been reaped by its parent. When kandev-kill
|
||||
# targets a non-leader backend whose parent is a sleeping launcher (not us), the
|
||||
# dead process lingers as a zombie — treat that as success, not failure.
|
||||
pid_is_alive() {
|
||||
local pid="$1" stat state
|
||||
[[ -d "/proc/$pid" ]] || return 1
|
||||
stat="$(cat "/proc/$pid/stat" 2>/dev/null || true)"
|
||||
[[ -n "$stat" ]] || return 1
|
||||
# The state char is the first field after the comm field, which is wrapped in
|
||||
# parens and may itself contain spaces — strip through the LAST ')' first.
|
||||
state="${stat##*) }"
|
||||
state="${state%% *}"
|
||||
[[ "$state" == "Z" ]] && return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- Terminate: SIGTERM first (cascades to agent children), then any stragglers
|
||||
# individually, then SIGKILL after the grace period. ---
|
||||
signal_tree() {
|
||||
local sig="$1"
|
||||
# If the backend leads its own process group, signal the whole group so
|
||||
# detached children cascade. Never signal a negative PID we don't lead.
|
||||
if [[ "$BACKEND_IS_LEADER" -eq 1 ]]; then
|
||||
kill -"$sig" -- -"$BACKEND_PID" 2>/dev/null || true
|
||||
fi
|
||||
# Always signal each discovered PID individually (the fallback when we don't
|
||||
# own the group, and a belt-and-suspenders pass when we do).
|
||||
for p in "${TREE[@]}"; do
|
||||
kill -"$sig" "$p" 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
signal_tree TERM
|
||||
|
||||
# Wait up to GRACE_SECONDS for the backend to exit.
|
||||
waited=0
|
||||
while pid_is_alive "$BACKEND_PID" && [[ "$waited" -lt "$GRACE_SECONDS" ]]; do
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
|
||||
if pid_is_alive "$BACKEND_PID"; then
|
||||
echo "kandev-kill: backend still alive after ${GRACE_SECONDS}s, sending SIGKILL."
|
||||
signal_tree KILL
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# Reap any remaining tracked PIDs with SIGKILL.
|
||||
for p in "${TREE[@]}"; do
|
||||
if pid_is_alive "$p"; then
|
||||
kill -KILL "$p" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
if pid_is_alive "$BACKEND_PID"; then
|
||||
echo "kandev-kill: WARNING — backend PID $BACKEND_PID is still present." >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
# Clean up pidfile(s).
|
||||
if [[ -n "$PIDFILE" ]]; then
|
||||
rm -f "$PIDFILE" "${PIDFILE%.pid}.web.pid" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "kandev-kill: done. Backend on port ${PORT:-?} (PID $BACKEND_PID) terminated."
|
||||
Reference in New Issue
Block a user