Add workspace runtime kill script
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Executable
+289
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Kill all local Paperclip workspace runtime service processes.
|
||||
#
|
||||
# This targets managed workspace services such as preview/dev commands started
|
||||
# from project or execution workspaces. Use scripts/kill-dev.sh for Paperclip
|
||||
# server processes.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/kill-workspaces.sh # kill workspace runtime services
|
||||
# scripts/kill-workspaces.sh --dry # preview what would be killed
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
DRY_RUN=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry|--dry-run|-n)
|
||||
DRY_RUN=true
|
||||
;;
|
||||
--help|-h)
|
||||
sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $arg" >&2
|
||||
echo "Usage: scripts/kill-workspaces.sh [--dry]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
REPO_PARENT="$(dirname "$REPO_ROOT")"
|
||||
CURRENT_PID="$$"
|
||||
CURRENT_PGID="$(ps -o pgid= -p "$CURRENT_PID" 2>/dev/null | tr -d '[:space:]' || true)"
|
||||
|
||||
expand_home() {
|
||||
local value="$1"
|
||||
if [[ "$value" == "~" ]]; then
|
||||
printf '%s\n' "$HOME"
|
||||
elif [[ "$value" == "~/"* ]]; then
|
||||
printf '%s\n' "$HOME/${value#"~/"}"
|
||||
else
|
||||
printf '%s\n' "$value"
|
||||
fi
|
||||
}
|
||||
|
||||
runtime_dirs=()
|
||||
declare -A seen_runtime_dirs=()
|
||||
|
||||
append_runtime_dir() {
|
||||
local dir="$1"
|
||||
[[ -d "$dir" ]] || return 0
|
||||
dir="$(cd "$dir" && pwd)"
|
||||
if [[ -z "${seen_runtime_dirs[$dir]:-}" ]]; then
|
||||
seen_runtime_dirs["$dir"]=1
|
||||
runtime_dirs+=("$dir")
|
||||
fi
|
||||
}
|
||||
|
||||
paperclip_home="$(expand_home "${PAPERCLIP_HOME:-$HOME/.paperclip}")"
|
||||
paperclip_instance_id="${PAPERCLIP_INSTANCE_ID:-default}"
|
||||
append_runtime_dir "$paperclip_home/instances/$paperclip_instance_id/runtime-services"
|
||||
|
||||
if [[ "${PAPERCLIP_KILL_WORKSPACES_ONLY_CURRENT:-}" != "1" ]]; then
|
||||
for dir in \
|
||||
"$HOME"/.paperclip/instances/*/runtime-services \
|
||||
"$HOME"/.paperclip-worktrees/instances/*/runtime-services \
|
||||
"$REPO_ROOT"/.paperclip/instances/*/runtime-services \
|
||||
"$REPO_ROOT"/.paperclip/runtime-services/instances/*/runtime-services
|
||||
do
|
||||
append_runtime_dir "$dir"
|
||||
done
|
||||
|
||||
for sibling_root in "$REPO_PARENT"/paperclip*; do
|
||||
[[ -d "$sibling_root" ]] || continue
|
||||
for dir in \
|
||||
"$sibling_root"/.paperclip/instances/*/runtime-services \
|
||||
"$sibling_root"/.paperclip/runtime-services/instances/*/runtime-services
|
||||
do
|
||||
append_runtime_dir "$dir"
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
record_lines=()
|
||||
if [[ ${#runtime_dirs[@]} -gt 0 ]]; then
|
||||
mapfile -t record_lines < <(node - "${runtime_dirs[@]}" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
function clean(value) {
|
||||
return String(value ?? "").replace(/[\t\r\n]+/g, " ");
|
||||
}
|
||||
|
||||
for (const dir of process.argv.slice(2)) {
|
||||
let entries = [];
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
||||
const filePath = path.resolve(dir, entry.name);
|
||||
let record;
|
||||
try {
|
||||
record = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!record || record.profileKind !== "workspace-runtime") continue;
|
||||
if (!Number.isInteger(record.pid) || record.pid <= 0) continue;
|
||||
|
||||
console.log([
|
||||
filePath,
|
||||
record.serviceKey,
|
||||
record.serviceName,
|
||||
record.pid,
|
||||
Number.isInteger(record.processGroupId) && record.processGroupId > 0 ? record.processGroupId : "",
|
||||
Number.isInteger(record.port) && record.port > 0 ? record.port : "",
|
||||
record.cwd,
|
||||
record.url,
|
||||
record.runtimeServiceId,
|
||||
].map(clean).join("\x1f"));
|
||||
}
|
||||
}
|
||||
NODE
|
||||
)
|
||||
fi
|
||||
|
||||
is_pid_running() {
|
||||
local pid="$1"
|
||||
kill -0 "$pid" 2>/dev/null
|
||||
}
|
||||
|
||||
is_group_running() {
|
||||
local pgid="$1"
|
||||
[[ -n "$pgid" && "$pgid" =~ ^[0-9]+$ && "$pgid" != "1" ]] || return 1
|
||||
[[ -z "$CURRENT_PGID" || "$pgid" != "$CURRENT_PGID" ]] || return 1
|
||||
kill -0 "-$pgid" 2>/dev/null
|
||||
}
|
||||
|
||||
target_is_running() {
|
||||
local type="$1"
|
||||
local id="$2"
|
||||
if [[ "$type" == "group" ]]; then
|
||||
is_group_running "$id"
|
||||
else
|
||||
is_pid_running "$id"
|
||||
fi
|
||||
}
|
||||
|
||||
signal_target() {
|
||||
local type="$1"
|
||||
local id="$2"
|
||||
local signal="$3"
|
||||
if [[ "$type" == "group" ]]; then
|
||||
kill "-$signal" "-$id" 2>/dev/null
|
||||
else
|
||||
kill "-$signal" "$id" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
active_files=()
|
||||
stale_files=()
|
||||
display_lines=()
|
||||
target_keys=()
|
||||
target_types=()
|
||||
target_ids=()
|
||||
declare -A seen_target_keys=()
|
||||
|
||||
for line in "${record_lines[@]}"; do
|
||||
IFS=$'\x1f' read -r file service_key service_name pid pgid port cwd url runtime_service_id <<< "$line"
|
||||
|
||||
target_type=""
|
||||
target_id=""
|
||||
if is_group_running "$pgid"; then
|
||||
target_type="group"
|
||||
target_id="$pgid"
|
||||
elif is_pid_running "$pid" && [[ "$pid" != "$CURRENT_PID" ]]; then
|
||||
target_type="pid"
|
||||
target_id="$pid"
|
||||
fi
|
||||
|
||||
if [[ -z "$target_id" ]]; then
|
||||
stale_files+=("$file")
|
||||
continue
|
||||
fi
|
||||
|
||||
active_files+=("$file")
|
||||
target_key="$target_type:$target_id"
|
||||
if [[ -z "${seen_target_keys[$target_key]:-}" ]]; then
|
||||
seen_target_keys["$target_key"]=1
|
||||
target_keys+=("$target_key")
|
||||
target_types+=("$target_type")
|
||||
target_ids+=("$target_id")
|
||||
fi
|
||||
|
||||
short_file="${file/#$HOME\//}"
|
||||
short_cwd="${cwd/#$HOME\//}"
|
||||
target_label="$target_type:$target_id"
|
||||
display_lines+=("$(printf " %-24s pid %-7s target %-14s port %-6s cwd %-45s registry %s" \
|
||||
"${service_name:-workspace-runtime}" "$pid" "$target_label" "${port:-"-"}" "${short_cwd:-"-"}" "$short_file")")
|
||||
if [[ -n "${url:-}" ]]; then
|
||||
display_lines+=("$(printf " %-24s url %s" "" "$url")")
|
||||
fi
|
||||
if [[ -n "${runtime_service_id:-}" ]]; then
|
||||
display_lines+=("$(printf " %-24s runtimeServiceId %s" "" "$runtime_service_id")")
|
||||
fi
|
||||
if [[ -n "${service_key:-}" ]]; then
|
||||
display_lines+=("$(printf " %-24s serviceKey %s" "" "$service_key")")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#active_files[@]} -eq 0 && ${#stale_files[@]} -eq 0 ]]; then
|
||||
echo "No Paperclip workspace runtime services found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ${#active_files[@]} -gt 0 ]]; then
|
||||
echo "Found ${#active_files[@]} Paperclip workspace runtime service record(s):"
|
||||
echo ""
|
||||
printf '%s\n' "${display_lines[@]}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [[ ${#stale_files[@]} -gt 0 ]]; then
|
||||
echo "Found ${#stale_files[@]} stale workspace runtime service registry record(s)."
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
stale_preview_limit=20
|
||||
stale_preview_count=0
|
||||
for file in "${stale_files[@]}"; do
|
||||
if (( stale_preview_count >= stale_preview_limit )); then
|
||||
remaining=$(( ${#stale_files[@]} - stale_preview_count ))
|
||||
echo " ... ${remaining} more stale record(s)"
|
||||
break
|
||||
fi
|
||||
echo " stale ${file/#$HOME\//}"
|
||||
((stale_preview_count += 1))
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "Dry run — re-run without --dry to kill these services and remove stale registry records."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ${#target_keys[@]} -gt 0 ]]; then
|
||||
echo "Sending SIGTERM to workspace runtime process targets..."
|
||||
for i in "${!target_keys[@]}"; do
|
||||
type="${target_types[$i]}"
|
||||
id="${target_ids[$i]}"
|
||||
if signal_target "$type" "$id" "TERM"; then
|
||||
echo " signaled $type $id"
|
||||
else
|
||||
echo " $type $id already gone"
|
||||
fi
|
||||
done
|
||||
|
||||
sleep 2
|
||||
|
||||
for i in "${!target_keys[@]}"; do
|
||||
type="${target_types[$i]}"
|
||||
id="${target_ids[$i]}"
|
||||
if target_is_running "$type" "$id"; then
|
||||
echo " $type $id still alive, sending SIGKILL..."
|
||||
signal_target "$type" "$id" "KILL" || true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ${#active_files[@]} -gt 0 || ${#stale_files[@]} -gt 0 ]]; then
|
||||
echo "Removing workspace runtime service registry records..."
|
||||
for file in "${active_files[@]:-}" "${stale_files[@]:-}"; do
|
||||
[[ -n "$file" ]] || continue
|
||||
rm -f "$file"
|
||||
echo " removed ${file/#$HOME\//}"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Done."
|
||||
Reference in New Issue
Block a user