# shellcheck shell=bash # shellcheck disable=SC2154 # All-caps globals (StracePidFile, StraceEnabledFile, etc.) # # are set by callers before sourcing this library. #============================================================================== # Copyright and license info is available in the LICENSE file included with # the Server Deployment Package (SDP), and also available online: # https://swarm.workshop.perforce.com/projects/perforce-software-sdp/view/main/LICENSE #------------------------------------------------------------------------------ #============================================================================== # Library: strace.lib # # Shared functions for diagnostic strace support in SDP service init scripts # (p4d_base, p4broker_base, p4p_base). # # Usage: # Source this file after p4_vars has been loaded (so $P4CLIB is defined): # # source "$P4CLIB/strace.lib" # # Before calling these functions, set the following globals: # # StraceEnabledFile - marker file path, e.g. $LOGS/.strace.p4d_1 # StracePidFile - strace PID file path, e.g. $LOGS/.strace.p4d_1.pid # StraceLog - strace output log base path, e.g. $LOGS/strace.p4d_1.log # A timestamp is appended at attachment time, e.g.: # $LOGS/strace.p4d_1.20260627_123456.log # Log - init log file path for appending status messages # ServiceName - service name used in log messages, e.g. p4d_1 # # Two strace modes are supported: # # Wrap mode (systemd only): run_service_under_strace # strace runs the service binary as its child process, capturing syscalls # from the very first instruction. Used when the service runs in the # foreground (systemd). In this mode strace cannot be stopped independently # from the service. # # Attach mode (non-systemd): attach_strace_if_requested # strace attaches to the already-running service after it daemonizes. Easy # to stop independently (kill $(cat $StracePidFile)), but early startup # syscalls (before the PID is known) will be missed. # # Callers are responsible for selecting the appropriate mode based on # $UseSystemd. See doc/Using_strace_for_SDP_Services.md for full details. #============================================================================== #------------------------------------------------------------------------------ # Function: run_service_under_strace "$binary" [args...] # # Runs the given command under strace in "wrap" mode. strace is interposed # between the init script and the service binary, so every syscall is captured # from the very first instruction — nothing is missed. # # This is the correct mode when the service runs in the foreground (systemd), # because the init script already blocks on the service binary; replacing it # with "strace ... binary" has zero additional complexity. # # In non-systemd (daemon) mode the service forks immediately and strace -f # would block the init script indefinitely; use attach_strace_if_requested # instead. # # Output file: # $StraceLog with a timestamp appended, e.g. strace.p4d_1.20260627_123456.log # The exact path is recorded in $Log. # # NOTE: In wrap mode strace is the parent process of the service. The only # way to stop strace is to stop the service normally (e.g. systemctl stop). #------------------------------------------------------------------------------ function run_service_under_strace { if ! command -v strace > /dev/null 2>&1; then echo "Warning: $StraceEnabledFile exists but 'strace' was not found in PATH; starting $ServiceName without strace." | tee -a "$Log" "$@" return fi local strace_custom_flags strace_custom_flags=$(tr -d '\n\r' < "$StraceEnabledFile" | xargs 2>/dev/null) local strace_flags="${strace_custom_flags:--f -tt}" local ts ts=$(date +%Y%m%d_%H%M%S) local effective_log="${StraceLog%.log}.${ts}.log" echo "Starting $ServiceName under strace (wrap mode, flags: $strace_flags); output: $effective_log" | tee -a "$Log" echo "Note: strace is the parent process in wrap mode; stop strace by stopping the service normally." | tee -a "$Log" # shellcheck disable=SC2086 strace $strace_flags -o "$effective_log" "$@" } #------------------------------------------------------------------------------ # Function: attach_strace_if_requested # # If $StraceEnabledFile exists, attaches strace to the already-running service # process and writes the strace PID to $StracePidFile. Intended for # non-systemd (daemon) mode where the service forks into the background and # the init script cannot block waiting for strace. # # Because strace attaches after the service has started, early startup syscalls # (from process start until the PID is discoverable) will be missed. For # complete coverage use run_service_under_strace (systemd only). # # Arguments: # $1 - Shell command string whose stdout is the service PID. Retried # up to 5 seconds while waiting for the process to appear. # Examples: # "cat \"$P4ROOT/server.pid\"" # p4d # "get_pids \"$P4PBIN\"" # p4p # "get_pids \"$P4BROKERCFG\"" # p4broker # # Strace flags: # Default flags are '-f -tt'. To override, place custom flags on a single # line in $StraceEnabledFile before starting the service. # # Output file: # $StraceLog with a timestamp appended, e.g. strace.p4d_1.20260627_123456.log # # NOTE: strace -f on a busy server generates large output and adds measurable # overhead. This feature is intended for diagnostic use only. #------------------------------------------------------------------------------ function attach_strace_if_requested { local pid_cmd="${1:?Usage: attach_strace_if_requested }" [[ -e "$StraceEnabledFile" ]] || return 0 if ! command -v strace > /dev/null 2>&1; then echo "Warning: $StraceEnabledFile exists but 'strace' was not found in PATH; skipping strace." | tee -a "$Log" return 0 fi # Use flags from the marker file if non-empty, otherwise apply defaults. local strace_custom_flags strace_custom_flags=$(tr -d '\n\r' < "$StraceEnabledFile" | xargs 2>/dev/null) local strace_flags="${strace_custom_flags:--f -tt}" # Wait up to 5 seconds for the service process to appear. local service_pid= local _i for _i in 1 2 3 4 5; do # shellcheck disable=SC2086 service_pid=$(eval $pid_cmd 2>/dev/null | awk '{print $1}') [[ -n "$service_pid" ]] && break sleep 1 done if [[ -z "$service_pid" ]]; then echo "Warning: $StraceEnabledFile exists but could not find a running $ServiceName process; skipping strace." | tee -a "$Log" return 0 fi local ts ts=$(date +%Y%m%d_%H%M%S) local effective_log="${StraceLog%.log}.${ts}.log" echo "Attaching strace (flags: $strace_flags) to $ServiceName pid $service_pid; output: $effective_log" | tee -a "$Log" # shellcheck disable=SC2086 nohup strace $strace_flags -o "$effective_log" -p "$service_pid" > /dev/null 2>&1 & local strace_pid=$! echo "$strace_pid" > "$StracePidFile" echo "strace running as pid $strace_pid. PID file: $StracePidFile" | tee -a "$Log" echo "To stop strace without affecting $ServiceName: kill \$(cat $StracePidFile)" | tee -a "$Log" } #------------------------------------------------------------------------------ # Function: stop_strace_if_running # # Stops the background strace process recorded in $StracePidFile (if any) # and removes the PID file. Used after a service stops in attach mode. # Safe to call even if strace has already exited (strace exits automatically # when its target process dies). # # Not applicable in wrap mode (strace exits with the service automatically). #------------------------------------------------------------------------------ function stop_strace_if_running { [[ -r "$StracePidFile" ]] || return 0 local strace_pid strace_pid=$(cat "$StracePidFile") if kill -0 "$strace_pid" > /dev/null 2>&1; then echo "Stopping strace (pid $strace_pid) for $ServiceName." | tee -a "$Log" kill "$strace_pid" 2>&1 | tee -a "$Log" else echo "strace (pid $strace_pid) for $ServiceName is no longer running." | tee -a "$Log" fi rm -f "$StracePidFile" echo "Removed $StracePidFile." | tee -a "$Log" }