#!/bin/sh # Copyright (c) 2025 Michael Greenberg # # Usage of this source code is governed by the GPL license. See the # LICENSE file in the root directory of this project's repository. # # https://github.com/mgree/mute MUTE_VERSION="v0.1.0" MUTE_COMMAND="${0##*/}" ################################################################################ # Cleanup (removes GDB script if it exists) ################################################################################ cleanup() { ec=$? [ -f "$MUTE_GDB_SCRIPT" ] && rm "$MUTE_GDB_SCRIPT" exit $ec } trap cleanup EXIT ################################################################################ # Debugging ################################################################################ debug() { ec=$? if [ "$MUTE_DEBUG" -gt 0 ] then echo "$MUTE_COMMAND: " "$@" fi return $? } ################################################################################ # Mutes a file descriptor unconditionally # # Will try to redirect to /dev/null before calling close ################################################################################ mute_unconditionally() { fd="$1" indent="$2" echo "${indent}# mute fd $fd unconditionally" echo "${indent}if $target > 0" echo "${indent} call (int) dup2($target, $fd)" echo "${indent}else" echo "${indent} call (int) close($fd)" echo "${indent}end" } ################################################################################ # Mutes a file descriptor if it's a tty ################################################################################ mute_if_tty() { fd="$1" indent="$2" fd_isatty="\$fd${fd}_isatty" echo "${indent}# mute fd $fd if it is a tty" "$fd" echo "${indent}set $fd_isatty = (int) isatty($fd)" echo "${indent}if $fd_isatty" mute_unconditionally "$fd" "${indent} " echo "${indent}end" } ################################################################################ # Mutes the fds of $PID in $FDS ################################################################################ main() { MUTE_GDB_SCRIPT="$(mktemp)" # start generating script exec 3>&1 1>"$MUTE_GDB_SCRIPT" echo "# mute GDB script (pid=$PID) $(date -Iseconds)" echo echo "# open $MUTE_TARGET to replace target fds" O_RDWR="$(printf "#include <fcntl.h>\n\nO_RDWR" | gcc -E - | tail -n 1)" O_CREAT="$(printf "#include <fcntl.h>\n\nO_CREAT" | gcc -E - | tail -n 1)" O_TRUNCAPPEND="$(printf "#include <fcntl.h>\n\n%s" "$MUTE_WRITEMODE" | gcc -E - | tail -n 1)" O_MODE="$(printf "0%o" $(( 0666 & ~$(umask) )) )" target="\$target" echo "set $target = (int) open(\"$MUTE_TARGET\", $O_RDWR | $O_CREAT | $O_TRUNCAPPEND, $O_MODE)" echo if [ "$MUTE_DEBUG" -gt 1 ] then echo "# debugging: show opened file descriptor" echo "print $target" echo fi for fd in $FDS do fds_nonempty=1 mute_"$MUTE_MODE" "$fd" echo done if ! [ "$fds_nonempty" ] then debug "no FDS in $FDS" >&3 exit 0 fi echo "# close original $MUTE_TARGET fd" echo "call (int) close($target)" # restore stdout exec 1>&3 3>&- if [ "$MUTE_DRY_RUN" ] || [ "$MUTE_DEBUG" -gt 1 ] then cat "$MUTE_GDB_SCRIPT" fi if [ "$MUTE_DRY_RUN" ] then return fi if [ "$MUTE_DEBUG" -gt 0 ] then debug "invoking GDB" gdb --pid="$PID" --nx --batch --command="$MUTE_GDB_SCRIPT" 2>&1 ec=$? debug "GDB done" return $? else gdb --pid="$PID" --nx --batch-silent --command="$MUTE_GDB_SCRIPT" 2>/dev/null fi } ################################################################################ # Argument parsing ################################################################################ usage() { cat >&2 <<EOF Usage: $MUTE_COMMAND [-tfandv] [-o TARGET] PID [FD ...] -t only close FDs if they are ttys (if no FDs specified, will close all tty FDs) -f close FDs unconditionally -a append to TARGET rather than truncating -n dry run; shows the GDB script to be used, but does not run it -d debug mode (shows debugging output on stdout; repeat to increase output) -v show version information and exit -o TARGET redirect FDs to TARGET [default: /dev/null] relative paths are relative to \`mute\`'s current directory, not PID's FD can be a decimal number or one of stdout, stderr, or stdin Running \`mute PID\` is the same as \`mute -t PID stdout stderr\`: FDs 1 and 2 will be closed if they are tty FDs. EOF } MUTE_DEBUG=0 while getopts ":tfandvo:h" opt do case "$opt" in (t) MUTE_MODE="if_tty";; (f) MUTE_MODE="unconditionally";; (a) MUTE_WRITEMODE="O_APPEND";; (n) MUTE_DRY_RUN=1;; (d) : $((MUTE_DEBUG+=1));; (o) if [ "$MUTE_TARGET" ] then echo "$MUTE_COMMAND: '-o $OPTARG' overrides previous '-o $MUTE_TARGET'" >&2 fi MUTE_TARGET="$OPTARG";; (h) usage exit 0;; (v) echo "$MUTE_COMMAND $MUTE_VERSION" echo echo "Copyright (c) 2025 Michael Greenberg, made available under GPLv3" exit 0;; (*) usage exit 2;; esac done shift $((OPTIND - 1)) if [ "$#" -eq 0 ] then usage exit 2 fi PID=$1 shift if [ "$#" -eq 0 ] then # no explicit FDs, so either auto-detect (bare -a) or set to 1 and 2 (default) if [ "$MUTE_MODE" = "if_tty" ] then # -a was set, no explicit FDs if ! [ -d "/proc/$PID/fd" ] then echo "$MUTE_COMMAND: could not find /proc/$PID/fd to auto-detect file descriptors" >&2 exit 1 fi FDS=$(cd "/proc/$PID/fd" || exit; ls) else # default to stdout and stderr FDS="1 2" fi else # explicit FDs while [ "$#" -gt 0 ] do fd="$1" shift case "$(echo "$fd" | tr "[:upper:]" "[:lower:]")" in (stdin) fd=0;; (stdout) fd=1;; (stderr) fd=2;; esac if [ "$fd" != "$(echo "$fd" | tr -dc 0-9)" ] then echo "$MUTE_COMMAND: '$fd' is not a valid file descriptor, ignoring" >&2 continue fi FDS="$FDS${FDS+ }$fd" done if ! [ "$FDS" ] then echo "$MUTE_COMMAND: no valid file descriptors, quitting" >&2 exit 2 fi fi # if MUTE_WRITEMODE was set but not MUTE_TARGET, issue a warning if [ "$MUTE_WRITE_MODE" ] && ! [ "$MUTE_TARGET" ] then echo "$MUTE_COMMAND: setting '-a' without specifying '-o TARGET' has no effect" >&2 fi # set defaults : "${MUTE_WRITEMODE=O_TRUNC}" : "${MUTE_TARGET=/dev/null}" : "${MUTE_MODE=if_tty}" # make paths relative to OUR cwd, not the inferior's case "$MUTE_TARGET" in ([!/]*) MUTE_TARGET="$PWD/$MUTE_TARGET";; esac if [ "$MUTE_DEBUG" -gt 0 ] then debug "PID=$PID DEBUG=$MUTE_DEBUG DRYRUN=$MUTE_DRYRUN MODE=$MUTE_MODE TARGET=/dev/null WRITEMODE=$MUTE_WRITEMODE FDS=$FDS" fi # check that $PID exists if ! [ "$MUTE_DRYRUN" ] then if ! type gdb >/dev/null 2>&1 then echo "$MUTE_COMMAND: gdb not found (is it installed and on your PATH?)" >&2 exit 2 fi if ! kill -0 "$PID" >/dev/null 2>&1 then echo "$MUTE_COMMAND: no such process $PID" >&2 exit 1 fi fi main