swift-nio/scripts/nio-diagnose

395 lines
7.9 KiB
Bash
Executable File

#!/bin/bash
##===----------------------------------------------------------------------===##
##
## This source file is part of the SwiftNIO open source project
##
## Copyright (c) 2021 Apple Inc. and the SwiftNIO project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##
set -u # do _NOT_ set -e here, some output is better than none...
PATH=/bin:/sbin:/usr/bin:/usr/sbin
output_file=""
function usage() {
echo >&2 "Usage: $0 [OPTIONS] PIDS..."
echo >&2
echo >&2 "Options:"
echo >&2 " -o OUTPUT_FILE"
echo >&2
echo >&2 "Examples:"
echo >&2 " $0 \$(pgrep MyService) # if your binary is called MyService"
echo >&2 " $0 12334 3731 313 # if your NIO binaries have pid 12234, 3731 and 313"
}
function log() {
echo >&2 "$*"
}
function info() {
log "info: $*"
}
function warning() {
log "WARNING: $*"
}
function die() {
log "ERROR: $*"
exit 1
}
function is_linux() {
if [[ "$(uname -s)" == Linux ]]; then
return 0
else
return 1
fi
}
complained_about_missing=()
function where_is() {
local where_is_var
local where_from
local found
found=false
for f in "${complained_about_missing[@]:+"${complained_about_missing[@]}"}"; do
if [[ "$f" == "$1" ]]; then
found=true
fi
done
if "$found"; then
return 0
fi
if is_linux; then
local where_is_netstat="sudo apt-get update && sudo apt-get install net-tools"
true "$where_is_netstat" # silence the unused warning
local where_is_lsof="sudo apt-get update && sudo apt-get install lsof"
true "$where_is_lsof" # silence the unused warning
fi
where_is_var="where_is_$1"
where_from=""
where_is_info="${!where_is_var:-}"
if [[ -n "$where_is_info" ]]; then
where_from=" (you might want to get it via \`${where_is_info}\`)"
fi
warning "\`$1\` not found$where_from"
complained_about_missing+=("$1")
}
while getopts "o:" opt; do
case "$opt" in
o)
output_file="$OPTARG"
true > "$output_file" # truncate the file
;;
/?)
usage
exit 1
;;
esac
done
shift $(( OPTIND - 1 ))
if ! is_linux; then
if [[ $# -lt 1 ]]; then
usage
die "At least one pid required"
fi
fi
if [[ -z "$output_file" ]]; then
output_file=$(mktemp "/tmp/nio-diagnose_$(date +%s)_XXXXXX")
fi
function output() {
if [[ "$output_file" == - ]]; then
echo "$*"
else
echo "$*" >> "$output_file"
fi
}
function stream_output() {
if [[ "$output_file" == - ]]; then
cat
else
cat >> "$output_file"
fi
}
function guess_nio_pids() {
declare -a nio_pids
if is_linux; then
nio_pids=()
while IFS=/ read -r _ _ pid _; do
nio_pids+=("$pid")
done < <(grep ^NIO-ELT /proc/*/task/*/comm 2> /dev/null || true)
for pid in "${nio_pids[@]+"${nio_pids[@]}"}"; do
echo "$pid"
done | sort | uniq
else
die "Sorry, can only guess the NIO pids on Linux"
fi
}
function nio_pids() {
if [[ $# -gt 0 ]]; then
for nio_pid in "$@"; do
echo "$nio_pid"
done
else
info "No pids provided, guessing NIO pids."
info "It's recommended that you pass the pids of your NIO programs as arguments."
guess_nio_pids
fi
}
function run_and_print() {
declare -ra command=( "$@" )
if ! command -v "${command[0]}" > /dev/null 2>&1; then
where_is "${command[0]}"
fi
output 'Command: `'"${command[*]}"'`'
output
output '```'
"${command[@]}" 2>&1 | stream_output
output '```'
output
}
function analyse_open_fds() {
local pid
declare -a command
pid=$1
command=( lsof -Pnp "$pid" )
output "### Open file descriptors (pid $pid)"
output
run_and_print "${command[@]}"
if ! which -v lsof > /dev/null 2> /dev/null; then
run_and_print ls -la "/proc/$pid/fd"
fi
}
function composite_command_ls_epoll() {
for fd_file in "/proc/$pid/fd"/*; do
if [[ "$(readlink "$fd_file")" =~ eventpoll ]]; then
fd=$(basename "$fd_file")
echo "epoll fd $fd"
echo "==========="
cat "/proc/$pid/fdinfo/$fd"
echo
fi
done
}
function composite_command_system_versions_darwin() {
date
uname -a
sw_vers
swift -version || true
id
}
function composite_command_system_versions_linux() {
date
uname -a
swift -version || true
id
}
function analyse_eventing_fds() {
local pid
declare -a command
pid=$1
if is_linux; then
command=( composite_command_ls_epoll "$pid" )
else
command=( lskq -p "$pid" )
fi
output "### Eventing fds state (pid $pid)"
output
run_and_print "${command[@]}"
}
function analyse_ulimit() {
declare -a command
command=( ulimit -a )
output "### ulimits"
output
run_and_print "${command[@]}"
}
function analyse_procs() {
declare -a command
if is_linux; then
command=( bash -c 'TERM=dumb top -H -n 1' )
else
command=( top -l 1 )
fi
output "### processes"
output
output "#### top"
output
run_and_print "${command[@]}"
output
output "#### ps"
output
run_and_print ps auxw
}
function analyse_system_versions() {
declare -a command
if is_linux; then
command=( composite_command_system_versions_linux )
else
command=( composite_command_system_versions_darwin )
fi
output "### System versions"
output
run_and_print "${command[@]}"
}
function analyse_syscalls() {
declare -a command
if is_linux; then
command=( grep -H ^ "/proc/$pid/task"/*/syscall )
else
command=( echo "Unsupported on Darwin" )
fi
output "### Syscalls by thread"
output
run_and_print "${command[@]}"
}
function analyse_thread_names() {
declare -a command
if is_linux; then
command=( grep -H ^ "/proc/$pid/task"/*/comm )
else
command=( echo "Unsupported on Darwin" )
fi
output "### Thread names"
output
run_and_print "${command[@]}"
}
function analyse_tcp_conns() {
declare -a command
if is_linux; then
command=( netstat --tcp -peona )
else
command=( netstat -n -p tcp -a )
fi
output "### TCP connections"
output
run_and_print "${command[@]}"
}
function analyse_udp() {
declare -a command
if is_linux; then
command=( netstat --udp -peona )
else
command=( netstat -n -p udp -a )
fi
output "### UDP"
output
run_and_print "${command[@]}"
}
function analyse_uds() {
declare -a command
if is_linux; then
command=( netstat --protocol=unix -peona )
else
command=( netstat -n -a -f unix )
fi
output "### Unix Domain Sockets"
output
run_and_print "${command[@]}"
}
function analyse_process() {
local pid
pid=$1
output "## Analysing pid $pid"
output
if ! kill -0 "$pid" 2> /dev/null; then
output "pid $pid is dead, skipping"
return 0
fi
analyse_open_fds "$pid"
analyse_eventing_fds "$pid"
analyse_thread_names "$pid"
analyse_syscalls "$pid"
}
number_of_nio_pids=0
output "# NIO diagnose ($(shasum "${BASH_SOURCE[0]}"))"
output
output "## System information"
output
analyse_system_versions
analyse_tcp_conns
analyse_udp
analyse_uds
analyse_procs
analyse_ulimit
while read -r nio_pid; do
number_of_nio_pids=$(( number_of_nio_pids + 1 ))
analyse_process "$nio_pid"
done < <(nio_pids "$@")
info "output written to $output_file"
if [[ "$number_of_nio_pids" -gt 0 ]]; then
exit 0
else
exit 1
fi