#!/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