aboutsummaryrefslogblamecommitdiff
path: root/cddl/usr.sbin/dwatch/dwatch
blob: 8d90d8173dfe626bfb9ec3afdd46c72b61c71be5 (plain) (tree)
















































                                                                            
                              








                                 





                                









                                  
                              








                                  

                                
































































                                                                               
                                                                            
















                                                                              
                                                                           






































































































































































































































































































































                                                                               
                             







































                                                                           




                                                    








                                                                            
























                                                                          

                                                                  










                                                                 






































































































































































































































































































                                                                               




                                                                         












                                                                             
                                                               




















































































































                                                                               




































                                                                                  













                                                                              





















































































































































































































































































                                                                               
                                                        







































                                                                   
            



                                                                                
#!/bin/sh
#-
# Copyright (c) 2014-2018 Devin Teske
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
############################################################ IDENT(1)
#
# $Title: Watch processes as they trigger a particular DTrace probe $
# $FreeBSD$
#
############################################################ CONFIGURATION

#
# DTrace pragma settings
#
DTRACE_PRAGMA="
	option quiet
	option dynvarsize=16m
	option switchrate=10hz
" # END-QUOTE

#
# Profiles
#
: ${DWATCH_PROFILES_PATH="/usr/libexec/dwatch:/usr/local/libexec/dwatch"}

############################################################ GLOBALS

VERSION='$Version: 1.4 $' # -V

pgm="${0##*/}" # Program basename

#
# Command-line arguments
#
PROBE_ARG=

#
# Command-line defaults
#
_MAX_ARGS=64		# -B num
_MAX_DEPTH=64		# -K num

#
# Command-line options
#
CONSOLE=		# -y
CONSOLE_FORCE=		# -y
[ -t 1 ] && CONSOLE=1	# -y
COUNT=0			# -N count
CUSTOM_DETAILS=		# -E code
CUSTOM_TEST=		# -t test
DEBUG=			# -d
DESTRUCTIVE_ACTIONS=	# -w
DEVELOPER=		# -dev
EXECNAME=		# -k name
EXECREGEX=		# -z regex
EXIT_AFTER_COMPILE=	# -e
FILTER=			# -r regex
PROBE_COALESCE=		# -F
GROUP=			# -g group
JID=			# -j jail
LIST=			# -l
LIST_PROFILES=		# -Q
MAX_ARGS=$_MAX_ARGS	# -B num
MAX_DEPTH=$_MAX_DEPTH	# -K num
ONELINE=		# -1
OUTPUT=			# -o file
OUTPUT_CMD=		# -O cmd
PID=			# -p pid
PROBE_TYPE=		# -f -m -n -P
PROFILE=		# -X profile
PSTREE=			# -R
QUIET=			# -q
TIMEOUT=		# -T time
TRACE=			# -x
USER=			# -u user
USE_PROFILE=		# -X profile
VERBOSE=		# -v

#
# Global exit status
#
SUCCESS=0
FAILURE=1

#
# Miscellaneous
#
ACTIONS=
EVENT_DETAILS=
EVENT_TAG='printf("%d.%d %s[%d]: ",
		this->uid0, this->gid0, execname, this->pid0);'
EVENT_TEST=
FILE=
ID=3
MODULE_CHECKED=
PROBE=
PSARGS=1
RGID=
RUID=
SUDO=
export SUDO_PROMPT="[sudo] Password:"
TITLE=\$Title:

############################################################ FUNCTIONS

ansi() { local fmt="$2 $4"; [ "$CONSOLE" ] && fmt="\\033[$1m$2\\033[$3m $4";
	shift 4; printf "$fmt\n" "$@"; }
die() { exec >&2; [ "$*" ] && echo "$pgm:" "$@"; exit $FAILURE; }
info() { [ "$QUIET" ] || ansi 35 "INFO" 39 "$@" >&2; }

usage()
{
	local optfmt="\t%-10s %s\n"
	exec >&2
	[ "$*" ] && printf "%s: %s\n" "$pgm" "$*"
	printf "Usage: %s [-1defFmnPqRvVwxy] [%s] [%s] [%s] [%s]\n" "$pgm" \
		"-B num" "-E code" "-g group" "-j jail"
	printf "\t      [%s] [%s] [%s] [%s] [%s] [%s]\n" \
		"-k name" "-K num" "-N count" "-o file" "-O cmd" "-p pid"
	printf "\t      [%s] [%s] [%s] [%s] [%s] [%s]\n" \
		"-r regex" "-t test" "-T time" "-u user" "-X profile" \
		"-z regex"
	printf "\t      probe[,...] [args ...]\n"
	printf "       %s -l [-fmnPqy] [-r regex] [probe ...]\n" "$pgm"
	printf "       %s -Q [-1qy] [-r regex]\n" "$pgm"
	printf "\n"
	printf "$optfmt" "-1" \
		"Print one line per process/profile (Default; disables \`-R')."
	printf "$optfmt" "-B num" \
		"Maximum process arguments to display (Default $_MAX_ARGS)."
	printf "$optfmt" "-d" \
		"Debug. Send dtrace(1) script to stdout instead of executing."
	printf "$optfmt" "-e" \
		"Exit after compiling request but prior to enabling probes."
	printf "$optfmt" "-E code" \
		"DTrace code for event details. If \`-', read from stdin."
	printf "$optfmt" "-f" \
		"Enable probe matching the specified function name."
	printf "$optfmt" "-F" \
		"Coalesce trace output by function."
	printf "$optfmt" "-g group" \
		"Group filter. Only show processes matching group name/gid."
	printf "$optfmt" "-j jail" \
		"Jail filter. Only show processes matching jail name/jid."
	printf "$optfmt" "-k name" \
		"Only show processes matching name."
	printf "$optfmt" "-K num" \
		"Maximum directory depth to display (Default $_MAX_DEPTH)."
	printf "$optfmt" "-l" \
		"List available probes on standard output and exit."
	printf "$optfmt" "-m" \
		"Enable probe matching the specified module name."
	printf "$optfmt" "-n" \
		"Enable probe matching the specified probe name."
	printf "$optfmt" "-N count" \
		"Exit after count matching entries (Default 0 for disabled)."
	printf "$optfmt" "-o file" \
		"Set output file. If \`-', the path \`/dev/stdout' is used."
	printf "$optfmt" "-O cmd" \
		"Execute cmd for each event."
	printf "$optfmt" "-p pid" \
		"Process id filter. Only show processes with matching pid."
	printf "$optfmt" "-P" \
		"Enable probe matching the specified provider name."
	printf "$optfmt" "-q" \
		"Quiet. Hide informational messages and all dtrace(1) errors."
	printf "$optfmt" "-Q" \
		"List available profiles in DWATCH_PROFILES_PATH and exit."
	printf "$optfmt" "-r regex" \
		"Filter. Only show blocks matching awk(1) regular expression."
	printf "$optfmt" "-R" \
		"Show parent, grandparent, and ancestor of process."
	printf "$optfmt" "-t test" \
		"Test clause (predicate) to limit events (Default none)."
	printf "$optfmt" "-T time" \
		"Timeout. Format is \`\#[smhd]' or simply \`\#' for seconds."
	printf "$optfmt" "-u user" \
		"User filter. Only show processes matching user name/uid."
	printf "$optfmt" "-v" \
		"Verbose. Show all errors from dtrace(1)."
	printf "$optfmt" "-V" \
		"Report dwatch version on standard output and exit."
	printf "$optfmt" "-w" \
		"Permit destructive actions (copyout*, stop, panic, etc.)."
	printf "$optfmt" "-x" \
		"Trace. Print \`<probe-id>' when a probe is triggered."
	printf "$optfmt" "-X profile" \
		"Load profile name from DWATCH_PROFILES_PATH."
	printf "$optfmt" "-y" \
		"Always treat stdout as console (enable colors/columns/etc.)."
	printf "$optfmt" "-z regex" \
		"Only show processes matching awk(1) regular expression."
	die
}

dtrace_cmd()
{
	local status stdout
	local timeout=

	if [ "$1" = "-t" ]; then
		shift
		[ "$TIMEOUT" ] && timeout=1
	fi

	exec 3>&1
	stdout=3

	#
	# Filter dtrace(1) stderr while preserving exit status
	#
	status=$(
		exec 4>&1
		to_status=4
		( trap 'echo $? >&$to_status' EXIT
			eval $SUDO ${timeout:+timeout \"\$TIMEOUT\"} dtrace \
				\"\$@\" 2>&1 ${QUIET:+2> /dev/null} >&$stdout
		) | dtrace_stderr_filter >&2
	)

	return $status
}

dtrace_stderr_filter()
{
	if [ "$VERBOSE" ]; then
		cat
		return
		# NOTREACHED
	fi

	awk ' # Start awk(1) stderr-filter
	/[[:digit:]]+ drops? on CPU [[:digit:]]+/ { next }
	/failed to write to <stdout>: No such file or directory/ { next }
	/failed to write to <stdout>: Broken pipe/ { next }
	/processing aborted: Broken pipe/ { next }
	/invalid address \(0x[[:xdigit:]]+\) in action #[[:digit:]]+/ { next }
	/out of scratch space in action #[[:digit:]]+/ { next }
	/^Bus error$/ { next }
	{ print; fflush() }
	' # END-QUOTE
}

expand_probe()
{
	local OPTIND=1 OPTARG flag
	local type=

	while getopts t: flag; do
		case "$flag" in
		t) type="$OPTARG" ;;
		esac
	done
	shift $(( $OPTIND - 1 ))

	local probe="$1"
	case "$probe" in
	*:*)
		echo "$probe"
		return $SUCCESS
		;;
	esac

	dtrace_cmd -l | awk -v probe="$probe" -v type="$type" '
	# Start awk(1) processor
	#################################################### BEGIN
	BEGIN { getline dtrace_header }
	#################################################### FUNCTIONS
	function dump(unused1,unused2) {
		if (n) {
			if (NcF[n] == 1) f = N2F[n]
			if (NcM[n] == 1) m = N2M[n]
			if (NcP[n] == 1) p = N2P[n]
		} else if (f) {
			if (FcM[f] == 1) m = F2M[f]
			if (FcP[f] == 1) p = F2P[f]
			if (FcN[f] == 0 && found) n = "entry"
		} else if (m) {
			if (McP[m] == 1) p = M2P[m]
		}
		printf "%s:%s:%s:%s\n", p, m, f, n
		exit !found
	}
	function inFMP() { return probe in F || probe in M || probe in P }
	function inNMP() { return probe in N || probe in M || probe in P }
	function inNFP() { return probe in N || probe in F || probe in P }
	function inNFM() { return probe in N || probe in F || probe in M }
	function diva(value, peerA, peerB, peerC) {
		return value >= peerA && value >= peerB && value >= peerC
	}
	#################################################### MAIN
	type == "name" && $NF != probe { next }
	type == "function" && NF >=4 && $(NF-1) != probe { next }
	type == "module" && NF == 5 && $(NF-2) != probe { next }
	type == "provider" && $2 != probe { next }
	type || $2 == probe || $3 == probe || $4 == probe || $5 == probe {
		P[_p = $2]++
		M[_m = (NF >= 5 ? $(NF-2) : "")]++
		F[_f = (NF >= 4 ? $(NF-1) : "")]++
		N[_n = $NF]++
		if (N2F[_n] != _f) NcF[_n]++; N2F[_n] = _f
		if (N2M[_n] != _m) NcM[_n]++; N2M[_n] = _m
		if (N2P[_n] != _p) NcP[_n]++; N2P[_n] = _p
		if (_n !~ /entry|return/) {
			if (F2N[_f] != _n) FcN[_f]++
			F2N[_f] = _n
		}
		if (F2M[_f] != _m) FcM[_f]++; F2M[_f] = _m
		if (F2P[_f] != _p) FcP[_f]++; F2P[_f] = _p
		if (M2P[_m] != _p) McP[_m]++; M2P[_m] = _p
	}
	#################################################### END
	END {
		if (type == "name")     dump(n = probe, found = probe in N)
		if (type == "function") dump(f = probe, found = probe in F)
		if (type == "module")   dump(m = probe, found = probe in M)
		if (type == "provider") dump(p = probe, found = probe in P)
		if (probe in N) {
			found = 1
			if (!inFMP()) dump(n = probe)
			if (diva(F[probe], N[probe], M[probe], P[probe]))
				dump(f = probe)
			if (diva(M[probe], N[probe], F[probe], P[probe]))
				dump(m = probe)
			if (diva(P[probe], N[probe], F[probe], M[probe]))
				dump(p = probe)
			dump(n = probe) # N is the diva
		} else if (probe in F) {
			found = 1
			if (!inNMP()) dump(f = probe)
			if (diva(N[probe], F[probe], M[probe], P[probe]))
				dump(n = probe)
			if (diva(M[probe], F[probe], N[probe], P[probe]))
				dump(m = probe)
			if (diva(P[probe], F[probe], N[probe], M[probe]))
				dump(p = probe)
			dump(f = probe) # F is the diva
		} else if (probe in M) {
			found = 1
			if (!inNFP()) dump(m = probe)
			if (diva(N[probe], M[probe], F[probe], P[probe]))
				dump(n = probe)
			if (diva(F[probe], M[probe], N[probe], P[probe]))
				dump(f = probe)
			if (diva(P[probe], M[probe], N[probe], F[probe]))
				dump(p = probe)
			dump(m = probe) # M is the diva
		} else if (probe in P) {
			found = 1
			if (!inNFM()) dump(p = probe)
			if (diva(N[probe], P[probe], F[probe], M[probe]))
				dump(n = probe)
			if (diva(F[probe], P[probe], N[probe], M[probe]))
				dump(f = probe)
			if (diva(M[probe], P[probe], N[probe], F[probe]))
				dump(m = probe)
			dump(p = probe) # P is the diva
		}
		if (!found) print probe
		exit !found
	}
	' # END-QUOTE
}

list_probes()
{
	local OPTIND=1 OPTARG flag
	local column=0 header="PROVIDER:MODULE:FUNCTION:NAME"
	local filter= quiet= type=

	while getopts f:qt: flag; do
		case "$flag" in
		f) filter="$OPTARG" ;;
		q) quiet=1 ;;
		t) type="$OPTARG" ;;
		esac
	done
	shift $(( $OPTIND - 1 ))

	if [ $# -eq 0 ]; then
		case "$type" in
		provider) column=1 header="PROVIDER" ;;
		module)   column=2 header="MODULE" ;;
		function) column=3 header="FUNCTION" ;;
		name)     column=4 header="NAME" ;;
		esac
	fi

	[ "$quiet" ] || echo "$header"

	local arg probe=
	for arg in "$@"; do
		arg=$( expand_probe -t "$type" -- "$arg" )
		probe="$probe${probe:+, }$arg"
	done

	dtrace_cmd -l${probe:+n "$probe"} | awk -v pattern="$(
		# Prevent backslashes from being lost
		echo "$filter" | awk 'gsub(/\\/,"&&")||1'
	)" -v want="$column" -v console="$CONSOLE" '
		BEGIN { getline dtrace_header }
		function ans(seq) { return console ? "\033[" seq "m" : "" }
		NF > 3 && $(NF-1) ~ /^#/ { next }
		!_[$0 = column[0] = sprintf("%s:%s:%s:%s",
			column[1] = $2,
			column[2] = (NF >= 5 ? $(NF-2) : ""),
			column[3] = (NF >= 4 ? $(NF-1) : ""),
			column[4] = $NF)]++ &&
			!__[$0 = column[want]]++ &&
			gsub(pattern, ans("31;1") "&" ans("39;22")) {
				print | "sort"
			}
		END { close("sort") }
	' # END-QUOTE

	exit $SUCCESS
}

list_profiles()
{
	local OPTIND=1 OPTARG flag
	local filter= oneline= quiet=

	while getopts 1f:q flag; do
		case "$flag" in
		1) oneline=1 ;;
		f) filter="$OPTARG" ;;
		q) quiet=1 ;;
		esac
	done
	shift $(( $OPTIND - 1 ))

	# Prevent backslashes from being lost
	filter=$( echo "$filter" | awk 'gsub(/\\/,"&&")||1' )

	# Build a list of profiles available
	local profiles
	profiles=$( { IFS=:
		for dir in $DWATCH_PROFILES_PATH; do
			[ -d "$dir" ] || continue
			for path in $dir/*; do
				[ -f "$path" ] || continue
				name="${path##*/}"
				[ "$name" = "${name%%[!0-9A-Za-z_-]*}" ] ||
					continue
				echo $name
			done
		done
	} | sort -u )

	# Get the longest profile name
	local longest_profile_name
	longest_profile_name=$( echo "$profiles" |
		awk -v N=0 '(L = length($0)) > N { N = L } END { print N }' )

	# Get the width of the terminal
	local max_size="$( stty size 2> /dev/null )"
	: ${max_size:=24 80}
	local max_width="${max_size#*[$IFS]}"

	# Determine how many columns we can display
	local x=$longest_profile_name ncols=1
	[ "$QUIET" ] || x=$(( $x + 8 )) # Accommodate leading tab character
	x=$(( $x + 3 + $longest_profile_name )) # Preload end of next column
	while [ $x -lt $max_width ]; do
		ncols=$(( $ncols + 1 ))
		x=$(( $x + 3 + $longest_profile_name ))
	done

	# Output single lines if sent to a pipe
	if [ "$oneline" ]; then
		echo "$profiles" | awk -v filter="$filter" -v cons="$CONSOLE" '
			function ans(s) { return cons ? "\033[" s "m" : "" }
			gsub(filter, ans("31;1") "&" ans("39;22"))
		' # END-QUOTE
		exit $SUCCESS
	fi

	[ "$quiet" ] || echo PROFILES:
	echo "$profiles" | awk \
		-v colsize=$longest_profile_name \
		-v console="$CONSOLE" \
		-v ncols=$ncols \
		-v quiet="$quiet" \
		-v filter="$filter" \
	' # Begin awk(1) processor
		function ans(seq) { return console ? "\033[" seq "m" : "" }
		BEGIN {
			row_item[1] = ""
			replace = ans("31;1") "&" ans("39;22")
			ansi_offset = length(replace) - 1
		}
		function print_row()
		{
			cs = colsize + ansi_offset * \
				gsub(filter, replace, row_item[1])
			printf "%s%-*s", quiet ? "" : "\t", cs, row_item[1]
			for (i = 2; i <= cur_col; i++) {
				cs = colsize + ansi_offset * \
					gsub(filter, replace, row_item[i])
				printf "   %-*s", cs, row_item[i]
			}
			printf "\n"
		}
		$0 ~ filter {
			n++
			cur_col = ((n - 1) % ncols) + 1
			row_item[cur_col] = $0
			if (cur_col == ncols) print_row()
		}
		END { if (cur_col < ncols) print_row() }
	' # END-QUOTE

	exit $SUCCESS
}

shell_escape()
{
	echo "$*" | awk 'gsub(/'\''/, "&\\\\&&")||1'
}

load_profile()
{
	local profile="$1"

	[ "$profile" ] ||
		die "missing profile argument (\`$pgm -Q' to list profiles)"

	local oldIFS="$IFS"
	local dir found=
	local ARGV=

	[ $COUNT -gt 0 ] &&			ARGV="$ARGV -N $COUNT"
	[ "$DEBUG" ] &&				ARGV="$ARGV -d"
	[ "$DESTRUCTIVE_ACTIONS" ] &&		ARGV="$ARGV -w"
	[ "$EXIT_AFTER_COMPILE" ] &&		ARGV="$ARGV -e"
	[ "$GROUP" ] &&				ARGV="$ARGV -g $GROUP"
	[ "$JID" ] &&				ARGV="$ARGV -j $JID"
	[ $MAX_ARGS -ne $_MAX_ARGS ] &&		ARGV="$ARGV -B $MAX_ARGS"
	[ $MAX_DEPTH -ne $_MAX_DEPTH ] &&	ARGV="$ARGV -K $MAX_DEPTH"
	[ "$ONELINE" ] &&			ARGV="$ARGV -1"
	[ "$PID" ] &&				ARGV="$ARGV -p $PID"
	[ "$PSTREE" ] &&			ARGV="$ARGV -R"
	[ "$QUIET" ] &&				ARGV="$ARGV -q"
	[ "$TIMEOUT" ] &&			ARGV="$ARGV -T $TIMEOUT"
	[ "$TRACE" ] &&				ARGV="$ARGV -x"
	[ "$USER" ] &&				ARGV="$ARGV -u $USER"
	[ "$VERBOSE" ] &&			ARGV="$ARGV -v"

	[ "$FILTER" ] &&
		ARGV="$ARGV -r '$( shell_escape "$FILTER" )'"
	[ "$EXECREGEX" ] &&
		ARGV="$ARGV -z '$( shell_escape "$EXECREGEX" )'"
	[ "$CUSTOM_DETAILS" ] &&
		ARGV="$ARGV -E '$( shell_escape "$EVENT_DETAILS" )'"
	[ "$CUSTOM_TEST" ] &&
		ARGV="$ARGV -t '$( shell_escape "$CUSTOM_TEST" )'"
	[ "$OUTPUT" ] &&
		ARGV="$ARGV -o '$( shell_escape "$OUTPUT" )'"
	[ "$OUTPUT_CMD" ] &&
		ARGV="$ARGV -O '$( shell_escape "$OUTPUT_CMD" )'"

	case "$PROBE_TYPE" in
	provider) ARGV="$ARGV -P" ;;
	  module) ARGV="$ARGV -m" ;;
	function) ARGV="$ARGV -f" ;;
	    name) ARGV="$ARGV -n" ;;
	esac

	IFS=:
	for dir in $DWATCH_PROFILES_PATH; do
		[ -d "$dir" ] || continue
		[ -f "$dir/$profile" ] || continue
		PROFILE="$profile" found=1
		info "Sourcing $profile profile [found in %s]" "$dir"
		. "$dir/$profile"
		break
	done
	IFS="$oldIFS"

	[ "$found" ] ||
		die "no module named \`$profile' (\`$pgm -Q' to list profiles)"
}

pproc()
{
	local OPTIND=1 OPTARG flag
	local P= N=0

	while getopts P: flag; do
		case "$flag" in
		P) P="$OPTARG" ;;
		esac
	done
	shift $(( OPTIND - 1 ))

	local proc=$1
	if [ ! "$proc" ]; then
		if [ "$P" = "0" ]; then
			proc="curthread->td_proc"
		else
			proc="this->proc ? this->proc->p_pptr : NULL"
		fi
	fi

	awk 'NR > 1 && $0 { $0 = "\t" $0 }
		gsub(/\\\t/, "\t") || 1
	' <<-EOFPREAMBLE
	this->proc = $proc;
	this->uid$P = this->proc ? this->proc->p_ucred->cr_uid : -1;
	this->gid$P = this->proc ? this->proc->p_ucred->cr_rgid : -1;
	this->pid$P = this->proc ? this->proc->p_pid : -1;
	this->jid$P = this->proc ? this->proc->p_ucred->cr_prison->pr_id : -1;

	this->p_args = this->proc ? this->proc->p_args : 0;
	this->ar_length = this->p_args ? this->p_args->ar_length : 0;
	this->ar_args = (char *)(this->p_args ? this->p_args->ar_args : 0);

	this->args$P = this->arg${P}_$N = this->ar_length > 0 ?
	\	this->ar_args : stringof(this->proc->p_comm);
	this->len = this->ar_length > 0 ? strlen(this->ar_args) + 1 : 0;
	this->ar_args += this->len;
	this->ar_length -= this->len;

	EOFPREAMBLE

	awk -v P=$P -v MAX_ARGS=$MAX_ARGS '
		$0 { $0 = "\t" $0 }
		buf = buf $0 "\n" { }
		END {
			while (++N <= MAX_ARGS) {
				$0 = buf
				gsub(/P/, P)
				gsub(/N/, N)
				gsub(/\\\t/, "\t")
				sub(/\n$/, "")
				print
			}
		}
	' <<-EOFARGS
	this->argP_N = this->ar_length > 0 ? this->ar_args : "";
	this->argsP = strjoin(this->argsP,
	\	strjoin(this->argP_N != "" ? " " : "", this->argP_N));
	this->len = this->ar_length > 0 ? strlen(this->ar_args) + 1 : 0;
	this->ar_args += this->len;
	this->ar_length -= this->len;

	EOFARGS

	N=$(( $MAX_ARGS + 1 ))
	awk 'sub(/^\\\t/, "\t") || 1, $0 = "\t" $0' <<-EOFPROC
	this->arg${P}_$N = this->ar_length > 0 ? "..." : "";
	this->args$P = strjoin(this->args$P,
	\	strjoin(this->arg${P}_$N != "" ? " " : "", this->arg${P}_$N));
	EOFPROC
}

pproc_dump()
{
	local OPTIND=1 OPTARG flag
	local verbose=

	while getopts v flag; do
		case "$flag" in
		v) verbose=1 ;;
		esac
	done
	shift $(( $OPTIND - 1 ))

	local P=$1
	if [ "$verbose" ]; then
		awk -v P=$P '
			BEGIN { printf "\t" }
			NR > 1 && $0 { $0 = "\t" $0 }
			buf = buf $0 "\n" { }
			END {
				$0 = buf
				if (P < 3) S = sprintf("%" 7-2*(P+1) "s", "")
				gsub(/S/, S)
				gsub(/B/, P < 3 ? "\\" : "")
				gsub(/\\\t/, "\t")
				sub(/\n$/, "")
				print
			}
		' <<-EOFPREAMBLE
		printf(" SB-+= %05d %d.%d %s\n",
		\	this->pid$P, this->uid$P, this->gid$P, this->args$P);
		EOFPREAMBLE
	else
		cat <<-EOFPREAMBLE
		printf("%s", this->args$P);
		EOFPREAMBLE
	fi
}

############################################################ MAIN

# If we're running as root, no need for sudo(8)
[ "$( id -u )" != 0 ] && type sudo > /dev/null 2>&1 && SUDO=sudo

#
# Process command-line options
#
while getopts 1B:deE:fFg:j:k:K:lmnN:o:O:p:PqQr:Rt:T:u:vVwxX:yz: flag; do
	case "$flag" in
	1) ONELINE=1 PSTREE= ;;
	B) MAX_ARGS="$OPTARG" ;;
	d) DEBUG=1 ;;
	e) EXIT_AFTER_COMPILE=1 ;;
	E) CUSTOM_DETAILS=1
	   EVENT_DETAILS="${EVENT_DETAILS%;}"
	   [ "$EVENT_DETAILS" ] && EVENT_DETAILS="$EVENT_DETAILS;
		printf(\" \");
		" # END-QUOTE
	   # Read event code from stdin if `-' is argument
	   [ "$OPTARG" = "-" ] && OPTARG=$( cat )
	   EVENT_DETAILS="$EVENT_DETAILS$OPTARG" ;;
	f) PROBE_TYPE=function ;;
	F) PROBE_COALESCE=1 ;;
	g) GROUP="$OPTARG" ;;
	j) JID="$OPTARG" ;;
	k) EXECNAME="$EXECNAME${EXECNAME:+ }$OPTARG"
	   case "$OPTARG" in
	   \**\*) name="${OPTARG%\*}"
		predicate="strstr(execname, \"${name#\*}\") != NULL" ;;
	   \**) name="${OPTARG#\*}"
		predicate="strstr(execname, \"$name\") == (execname +"
		predicate="$predicate strlen(execname) - ${#name})" ;;
	   *\*) predicate="strstr(execname, \"${OPTARG%\*}\") == execname" ;;
	   *) predicate="execname == \"$OPTARG\""
	   esac
	   EVENT_TEST="$predicate${EVENT_TEST:+ ||
		($EVENT_TEST)}" ;;
	K) MAX_DEPTH="$OPTARG" ;;
	l) LIST=1 ;;
	m) PROBE_TYPE=module ;;
	n) PROBE_TYPE=name ;;
	N) COUNT="$OPTARG" ;;
	o) OUTPUT="$OPTARG" ;;
	O) OUTPUT_CMD="$OPTARG" ;;
	p) PID="$OPTARG" ;;
	P) PROBE_TYPE=provider ;;
	q) QUIET=1 ;;
	Q) LIST_PROFILES=1 ;;
	r) FILTER="$OPTARG" ;;
	R) PSTREE=1 ;;
	t) CUSTOM_TEST="${CUSTOM_TEST:+($CUSTOM_TEST) && }$OPTARG" ;;
	T) TIMEOUT="$OPTARG" ;;
	u) USER="$OPTARG" ;;
	v) VERBOSE=1 ;;
	V) vers="${VERSION#\$*[:\$]}"
	   vers="${vers% \$}"
	   printf "%s: %s\n" "$pgm" "${vers# }"
	   exit ;;
	w) DESTRUCTIVE_ACTIONS=1 ;;
	x) TRACE=1 ;;
	X) USE_PROFILE=1 PROFILE="$OPTARG" ;;
	y) CONSOLE=1 CONSOLE_FORCE=1 ;;
	z) EXECREGEX="$OPTARG" ;;
	*) usage
	   # NOTREACHED
	esac
done
shift $(( $OPTIND - 1 ))

#
# List probes if `-l' was given
#
[ "$LIST" ] &&
	list_probes -f "$FILTER" ${QUIET:+-q} -t "$PROBE_TYPE" -- "$@"
	# NOTREACHED

#
# List profiles if `-Q' was given
#
[ "$LIST_PROFILES" ] &&
	list_profiles ${ONELINE:+-1} -f "$FILTER" ${QUIET:+-q}
	# NOTREACHED

#
# Validate number of arguments
#
if [ ! "$PROFILE" ]; then
	# If not given `-X profile' then a probe argument is required
	[ $# -gt 0 ] || usage # NOTREACHED
fi

#
# Validate `-N count' option argument
#
case "$COUNT" in
"") usage "-N option requires a number argument" ;; # NOTREACHED
*[!0-9]*) usage "-N argument must be a number" ;; # NOTREACHED
esac

#
# Validate `-B num' option argument
#
case "$MAX_ARGS" in
"") usage "-B option requires a number argument" ;; # NOTREACHED
*[!0-9]*) usage "-B argument must be a number" ;; # NOTREACHED
esac

#
# Validate `-K num' option argument
#
case "$MAX_DEPTH" in
"") usage "-K option requires a number argument" ;; # NOTREACHED
*[!0-9]*) usage "-K argument must be a number" ;; # NOTREACHED
esac

#
# Validate `-j jail' option argument
#
case "$JID" in
"") : fall through ;;
*[!0-9]*) JID=$( jls -j "$JID" jid ) || exit ;;
esac

#
# Validate `-u user' option argument
#
case "$USER" in
"") : fall through ;;
*[![:alnum:]_-]*) RUID="$USER" ;;
*[!0-9]*) RUID=$( id -u "$USER" 2> /dev/null ) || die "No such user: $USER" ;;
*) RUID=$USER
esac

#
# Validate `-g group' option argument
#
case "$GROUP" in
"") : fall-through ;;
*[![:alnum:]_-]*) RGID="$GROUP" ;;
*[!0-9]*)
	RGID=$( getent group | awk -F: -v group="$GROUP" '
		$1 == group { print $3; exit found=1 }
		END { exit !found }
	' ) || die "No such group: $GROUP" ;;
*) RGID=$GROUP
esac

#
# Expand probe argument into probe(s)
#
case "$1" in
-*) : Assume dtrace options such as "-c cmd" or "-p pid" ;; # No probe(s) given
*)
	PROBE_ARG="$1"
	shift
esac
if [ "$PROBE_ARG" ]; then
	oldIFS="$IFS"
	IFS="$IFS,"
	for arg in $PROBE_ARG; do
		arg=$( expand_probe -t "$PROBE_TYPE" -- "$arg" )
		PROBE="$PROBE${PROBE:+, }$arg"
	done
	IFS="$oldIFS"
fi

#
# Developer switch
#
[ "$DEBUG" -a "$EXIT_AFTER_COMPILE" -a "$VERBOSE" ] && DEVELOPER=1 DEBUG=

#
# Set default event details if `-E code' was not given
#
[ "$CUSTOM_DETAILS" ] || EVENT_DETAILS=$( pproc_dump 0 )

#
# Load profile if given `-X profile'
#
[ "$USE_PROFILE" ] && load_profile "$PROFILE"
[ "$PROBE" ] || die "PROBE not defined by profile and none given as argument"

#
# Show the user what's being watched
#
[ "$DEBUG$EXIT_AFTER_COMPILE" ] || info "Watching '$PROBE' ..."

#
# Header for watched probe entry
#
case "$PROBE" in
*,*) : fall-through ;;
*:execve:entry|execve:entry)
	ACTIONS=$( awk 'gsub(/\\\t/, "\t") || 1' <<-EOF
		$PROBE /* probe ID $ID */
		{${TRACE:+
		\	printf("<$ID>");}
		\	this->caller_execname = execname;
		}
		EOF
	)
	PROBE="${PROBE%entry}return"
	ID=$(( $ID + 1 ))
	EVENT_TEST="execname != this->caller_execname${EVENT_TEST:+ &&
		($EVENT_TEST)}"
	EVENT_TAG='printf("%d.%d %s[%d]: ",
		this->uid1, this->gid1, this->caller_execname, this->pid1);'
	;;
esac

#
# Jail clause/predicate
#
if [ "$JID" ]; then
	prison_id="curthread->td_proc->p_ucred->cr_prison->pr_id"
	EVENT_TEST="$prison_id == $JID${EVENT_TEST:+ &&
		($EVENT_TEST)}"
fi

#
# Custom test clause/predicate
#
if [ "$CUSTOM_TEST" ]; then
	case "$EVENT_TEST" in
	"") EVENT_TEST="$CUSTOM_TEST" ;;
	 *) EVENT_TEST="$EVENT_TEST &&
		($CUSTOM_TEST)"
	esac
fi

#
# Make sure dynamic code has trailing semi-colons if non-NULL
#
EVENT_TAG="${EVENT_TAG%;}${EVENT_TAG:+;}"
EVENT_DETAILS="${EVENT_DETAILS%;}${EVENT_DETAILS:+;}"

#
# DTrace script
#
# If `-d' is given, script is sent to stdout for debugging
# If `-c count", `-g group', `-r regex', or `-u user' is given, run script with
# dtrace and send output to awk(1) post-processor (making sure to preserve the
# exit code returned by dtrace invocation). Otherwise, simply run script with
# dtrace and then exit.
#
exec 9<<EOF
$PROBE /* probe ID 2 */
{${TRACE:+
	printf("<2>");
}
	/*
	 * Examine process, parent process, and grandparent process details
	 */

	/******************* CURPROC *******************/

	$( pproc -P0 )

	/******************* PPARENT *******************/

	$( if [ "$PSTREE" ]; then pproc -P1; else echo -n \
	"this->proc = this->proc ? this->proc->p_pptr : NULL;
	this->pid1 = this->proc ? this->proc->p_pid : -1;
	this->uid1 = this->proc ? this->proc->p_ucred->cr_uid : -1;
	this->gid1 = this->proc ? this->proc->p_ucred->cr_rgid : -1;
	this->jid1 = this->proc ? this->proc->p_ucred->cr_prison->pr_id : -1;"
	fi )

	/******************* GPARENT *******************/

	$( [ "$PSTREE" ] && pproc -P2 )

	/******************* APARENT *******************/

	$( [ "$PSTREE" ] && pproc -P3 )
}
EOF
PSARGS_ACTION=$( cat <&9 )
[ "$OUTPUT" -a ! "$CONSOLE_FORCE" ] && CONSOLE=
{
	if [ "$DEBUG" ]; then
		# Send script to stdout
		cat
		exit
	fi

	if [ "$CUSTOM_TEST$EXECNAME$JID$OUTPUT$TIMEOUT$TRACE$VERBOSE" -a \
	    ! "$QUIET" ]
	then
		msg=Setting
		[ "$CUSTOM_TEST" ] && msg="$msg test: $CUSTOM_TEST"
		[ "$EXECNAME" ] && msg="$msg execname: $EXECNAME"
		[ "$JID" ] && msg="$msg jid: $JID"
		[ "$OUTPUT" ] && msg="$msg output: $OUTPUT"
		[ "$TIMEOUT" ] && msg="$msg timeout: $TIMEOUT"
		[ "$TRACE" ] && msg="$msg trace: $TRACE"
		[ "$VERBOSE" ] && msg="$msg verbose: $VERBOSE"
		info "$msg"
	fi

	exec 3>&1
	console_stdout=3

	#
	# Developer debugging aide
	#
	if [ "$DEVELOPER" ]; then
		#
		# Run, capture the error line, and focus it
		#
		# Example error text to capture line number from:
		# 	dtrace: failed to compile script /dev/stdin: line 669: ...
		#
		errline=
		stdin_buf=$( cat )
		stderr_buf=$( echo "$stdin_buf" |
			dtrace_cmd -t -es /dev/stdin "$@" 2>&1 > /dev/null )
		status=$?
		if [ "$stderr_buf" ]; then
			errline=$( echo "$stderr_buf" | awk '
				BEGIN {
					ti = "\033[31m"
					te = "\033[39m"
				}
				{ line = $0 }
				sub(/.*: line /, "") && sub(/:.*/, "") {
					print # to errline
					sub("line " $0, ti "&" te, line)
				}
				{ print line > "/dev/stderr" }
			' 2>&3 )
		fi
		if  [ "$errline" ]; then
			echo "$stdin_buf" | awk -v line="${errline%%[^0-9]*}" '
				BEGIN {
					start = line < 10 ? 1 : line - 10
					end = line + 10
					slen = length(sprintf("%u", start))
					elen = length(sprintf("%u", end))
					N = elen > slen ? elen : slen
					ti[line] = "\033[31m"
					te[line] = "\033[39m"
					fmt = "%s%*u %s%s\n"
				}
				NR < start { next }
				NR == start, NR == end {
					printf(fmt, ti[NR], N, NR, $0, te[NR])
				}
				NR > end { exit }
			' # END-QUOTE
		fi
		exit $status
	fi

	if [ $COUNT -eq 0 -a ! "$EXECREGEX$FILTER$GROUP$OUTPUT_CMD$PID$USER" ]
	then
		case "$OUTPUT" in
		-) output_path=/dev/stdout ;;
		*) output_path="$OUTPUT"
		esac

		# Run script without pipe to awk post-processor
		dtrace_cmd -t \
			${DESTRUCTIVE_ACTIONS:+-w} \
			${EXIT_AFTER_COMPILE:+-e} \
			${OUTPUT:+-o "$output_path"} \
			-s /dev/stdin \
			"$@"
		exit
	fi

	# Prevent backslashes from being lost
	FILTER=$( echo "$FILTER" | awk 'gsub(/\\/,"&&")||1' )
	EXECREGEX=$( echo "$EXECREGEX" | awk 'gsub(/\\/,"&&")||1' )

	if [ ! "$QUIET" ]; then
		msg=Filtering
		[ "$EXECREGEX" ] && msg="$msg execregex: $EXECREGEX"
		[ "$FILTER" ] && msg="$msg filter: $FILTER"
		[ "$GROUP" ] && msg="$msg group: $GROUP"
		[ "$OUTPUT_CMD" ] && msg="$msg cmd: $OUTPUT_CMD"
		[ "$PID" ] && msg="$msg pid: $PID"
		[ "$USER" ] && msg="$msg user: $USER"
		[ $COUNT -gt 0 ] && msg="$msg count: $COUNT"
		info "$msg"
	fi

	#
	# Send script output to post-processor for filtering
	#
	status=$(
		exec 4>&1
		to_status=4
		( exec 5>&1; to_dtrace_stderr_filter=5; (
			trap 'echo $? >&$to_status' EXIT
			eval $SUDO ${TIMEOUT:+timeout \"\$TIMEOUT\"} dtrace \
				${EXIT_AFTER_COMPILE:+-e} \
				${DESTRUCTIVE_ACTIONS:+-w} \
				-s /dev/stdin \
				\"\$@\" \
				2>&$to_dtrace_stderr_filter \
				${QUIET:+2> /dev/null}
		) | $SUDO awk \
			-v cmd="$OUTPUT_CMD" \
			-v console="$CONSOLE" \
			-v count=$COUNT \
			-v execregex="$EXECREGEX" \
			-v filter="$FILTER" \
			-v gid="$RGID" \
			-v output="$OUTPUT" \
			-v pid="$PID" \
			-v pstree=$PSTREE \
			-v quiet=$QUIET \
			-v tty=$( ps -o tty= -p $$ ) \
			-v uid="$RUID" \
		' # Start awk(1) post-processor
		############################################ BEGIN
		BEGIN {
			true = 1
			ansi = "(\\033\\[[[:digit:];]+m)?"
			num = year = day = "[[:digit:]]+"
			month = "[[:alpha:]]+"
			date = year " " month " +" day
			time = "[012][0-9]:[0-5][0-9]:[0-5][0-9]"
			date_time = ansi date " +" time ansi
			name1 = "[^\\[]*"
			name2 = "[^\\n]*"
			if (output == "-")
				output = "/dev/stdout"

			#
			# Field definitions
			#
			nexecmatches = 2
			execstart[1] = sprintf( \
				"^(%s) (%s)\\.(%s) (%s)\\[(%s)\\]: ",
				date_time, num, num, name1, num)
			execstart[2] = sprintf( \
				"\\n +\\\\?-\\+= (%s) (%s)\\.(%s) ",
				num, num, num)
			npidmatches = 2
			pidstart[1] = sprintf("^(%s) (%s)\\.(%s) (%s)\\[",
				date_time, num, num, name1)
			pidstart[2] = "\\n +\\\\?-\\+= "
			pidpreen[2] = "^0*"
			piddeflt[2] = "0"
			ngidmatches = 2
			gidstart[1] = sprintf("^(%s) (%s)\\.", date_time, num)
			gidstart[2] = sprintf("\\n +\\\\?-\\+= (%s) (%s)\\.",
				ansi num ansi, num)
			nuidmatches = 2
			uidstart[1] = sprintf("^(%s) ", date_time)
			uidstart[2] = sprintf("\\n +\\\\?-\\+= (%s) ",
				ansi num ansi)
		}
		############################################ FUNCTIONS
		function strip(s) { gsub(/\033\[[0-9;]*m/, "", s); return s }
		function esc(str) { gsub(/'\''/, "&\\\\&&", str); return str }
		function arg(str) { return "'\''" esc(str) "'\''" }
		function env(var, str) { return var "=" arg(str) " " }
		function ans(seq) { return console ? "\033[" seq "m" : "" }
		function runcmd() {
			return system(sprintf("%s/bin/sh -c %s",
				env("TAG", strip(tag)) \
					env("DETAILS", strip(details)),
				arg(cmd)))
		}
		function filter_block() {
			if (length(lines) < 1) return 0
			block_match = 0
			newstr = ""
			start = 1
			if (match(lines, "^(" date_time ") ")) {
				newstr = newstr substr(lines, 1,
					RSTART + RLENGTH - 1)
				start = RSTART + RLENGTH
			}
			replace = ans("31;1") "&" ans("39;22")
			workstr = substr(lines, start)
			if (gsub(filter, replace, workstr)) block_match = 1
			lines = newstr workstr
			return block_match
		}
		function filter_field(startre, fieldre, matchre, isword,
			preenre, defaultstr)
		{
			if (length(lines) < 1) return 0
			field_match = 0
			newstr = ""
			start = 1
			while ((workstr = substr(lines, start)) &&
				(workstr ~ (startre fieldre)))
			{
				match(workstr, startre)
				start += end = RSTART + RLENGTH - 1
				newstr = newstr substr(workstr, 1, end)
				workstr = substr(workstr, end + 1)
				match(workstr, fieldre)
				start += end = RSTART + RLENGTH - 1
				field = matchstr = substr(workstr, 1, end)
				sub(preenre, "", matchstr)
				if (!matchstr) matchstr = defaultstr
				if (isword) {
					if (match(matchstr, matchre) &&
						RSTART == 1 &&
						RLENGTH == length(matchstr)) {
						field_match = 1
						field = ans(7) field ans(27)
					}
				} else {
					replace = ans(7) "&" ans(27)
					if (gsub(matchre, replace, matchstr)) {
						field_match = 1
						field = matchstr
					}
				}
				newstr = newstr field
			}
			lines = newstr workstr
			return field_match
		}
		function dump() {
			lines = block
			block = ""
			found = 0
			if (execregex != "") {
				for (n = 1; n <= nexecmatches; n++)
					if (filter_field(execstart[n], name2,
						execregex)) found = 1
				if (!found) return
			}
			if (pid != "") {
				for (n = 1; n <= npidmatches; n++)
					if (filter_field(pidstart[n], num, pid,
						true, pidpreen[n],
						piddeflt[n])) found = 1
				if (!found) return
			}
			if (gid != "") {
				for (n = 1; n <= ngidmatches; n++)
					if (filter_field(gidstart[n], num,
						gid, true)) found = 1
				if (!found) return
			}
			if (uid != "") {
				for (n = 1; n <= nuidmatches; n++)
					if (filter_field(uidstart[n], num,
						uid, true)) found = 1
				if (!found) return
			}
			if (filter != "" && !filter_block()) return
			if (lines) {
				stdout = 1
				if (output) {
					stdout = 0
					if (!console) lines = strip(lines)
					print lines > output
				} else if (cmd) {
					if (!quiet) print lines
					tag = details = lines
					sub(/: .*/, "", tag)
					sub(/.*: /, "", details)
					if (!console) tag = strip(tag)
					runcmd()
				} else print lines
			}
			fflush()
			++matches
		}
		############################################ MAIN
		{ block = (block ? block "\n" : block) $0 }
		!pstree { dump() }
		$0 ~ sprintf("^%6s\\\\-\\+= %s ", "", num) { dump() }
		count && matches >= count { exit }
		############################################ END
		END {
			dump()
			system(sprintf("pkill -t %s dtrace %s", tty,
				quiet ? "2> /dev/null" : ""))
		}
		' >&$console_stdout ) | dtrace_stderr_filter >&2
	) # status
	exit $status

} <<EOF
#!/usr/sbin/dtrace -s
/* -
 * Copyright (c) 2014-2018 Devin Teske <dteske@FreeBSD.org>
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS \`\`AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * $TITLE dtrace(1) script to log process(es) triggering $PROBE $
 * \$FreeBSD$
 */

$( echo "$DTRACE_PRAGMA" | awk '
	!/^[[:space:]]*(#|$)/, sub(/^[[:space:]]*/, "#pragma D ")||1
' )

int console;

dtrace:::BEGIN { console = ${CONSOLE:-0} } /* probe ID 1 */

/*********************************************************/

${PSARGS:+$PSARGS_ACTION}
${ACTIONS:+
/*********************************************************/

$ACTIONS
}
/*********************************************************/

$PROBE${EVENT_TEST:+ / $EVENT_TEST /} /* probe ID $ID */
{${TRACE:+
	printf("<$ID>");
}
	/***********************************************/

	printf("%s%Y%s ",
		console ? "\033[32m" : "",
		walltimestamp,
		console ? "\033[39m" : "");

	/****************** EVENT_TAG ******************/

	${EVENT_TAG#[[:space:]]}
${PROBE_COALESCE:+
	/**************** PROBE_COALESCE ***************/

	printf("%s%s:%s:%s:%s ", probename == "entry" ? "-> " :
			probename == "return" ? "<- " :
			probename == "start" ? "-> " :
			probename == "done" ? "<- " : " | ",
		probeprov, probemod, probefunc, probename);
}
	/**************** EVENT_DETAILS ****************/

	${EVENT_DETAILS#[[:space:]]}

	/***********************************************/

	printf("\\n");
${PSTREE:+
	/*
	 * Print process, parent, grandparent, and ancestor details
	 */
$(	pproc_dump -v 3
	pproc_dump -v 2
	pproc_dump -v 1
	pproc_dump -v 0
)}
}
EOF
# NOTREACHED

################################################################################
# END
################################################################################