#!/usr/unsupported/bin/bash

function usage ()
{
    if [ $# -gt 0 ]; then
       echo -e "${progname}: $*\n" 1>&2
    fi

    shcat 1>&2 <<EOF
Usage: ${progname} {-D} {-e maintainer} {-h} {-t time} {-v} {-x d1} {-x d2}
	{-x ...} [dir1] {dir2} {...}
	{--debug} {--errors-to=maintainer} {--help} {--time=time} {--verbose}
	{--except d1} {--except d2} {--except ...} [dir1] {dir2} {...}

   Options in braces are optional.  Those in brackets are required. 

   Time specifications must have units after the value, e.g. "d" for days,
   "m" for minutes.  Thus, a valid time string might be "1w2d12h" which
   means look for files older than 1 week, 2 days, and 12 hours (9.5 days).

   Default time is 1 day.  Default maintainer is ${default_maintainer}.

-D, --debug                  Turn on shell debugging ("set -x").
-e, --errors-to MAINTAINER   Stderr is reported to maintainer via email.
                             If MAINTAINER is set to "maintainer", the
                             default maintainer is assumed (see above). 
-h, --help                   You're looking at it.
-t, --time TIME              Only PRs older than TIME will be checked
			     for locks.  See above for info on time format. 
-v, --verbose                Chatter away while working.
-x, --except                 subdirectories not to search.  

EOF

    exit 1
}

function main ()
{
 local find_args="-mtime"
 local seconds
 local age_unit="day"
 local age
 local except_list 

    initialize_variables "$@"
    parse_command_args "$@"
    shift $?

    test -n "${debug+set}" && set -x

    if [ $# -eq 0 ]; then
       usage "Specify at least one pathname"
    fi

    if ! seconds=$(timefmt_to_seconds "${time}") ; then
       exit 1
    fi

    age=$(seconds_to_days ${seconds})
    if [ -z "${age}" -o "${age}" -lt 1 ]; then
       find_args="-mmin"
       age_unit="minute"
       age=$(seconds_to_minutes ${seconds})
    fi

    if [ -n "${exceptions}" ]; then
       # Construct exception list
       for dir in ${exceptions} ; do
          except_list="${except_list} ! -path ${dir}/*"
       done
    fi

    ( 
      set -o noglob; 
      exec find "$@" ${except_list} -type f -name "*.lock" ${find_args} "+${age}" -print
    ) | find_locks

    send_reports
    mail_stderr_to_maintainer
}

function initialize_variables ()
{
    # Bash magic.  If this variable is bound, then globbing patterns which
    # don't actually match anything will result in nothing at all, rather
    # than the pattern themselves. 
    allow_null_glob_expansion=

    progname="${0##*/}"
    progname_arguments="$*"  # Save them for stderr report

    bq="\`"  # To prevent hairy quoting and escaping later.
    eq="'"

    # Path should be able to catch sendmail, which is usually in /usr/lib
    export PATH="/usr/latest/bin:${PATH}:/usr/lib"

    default_maintainer="brendan"
    maintainer="${default_maintainer}"

    time=1d	   # Complain about locks older than 1 day, by default. 

    hostname=$(hostname)

    tmpdir="/tmp/${progname}$$"
    while [ -e "${tmpdir}" ]; do
       tmpdir="/tmp${progname}${RANDOM}"
    done

    TRAP_SIGNALS="EXIT SIGHUP SIGINT SIGQUIT SIGTERM"
    trap 'cleanup_and_exit' ${TRAP_SIGNALS}

    mkdir ${tmpdir} || exit 1
}

function parse_command_args ()
{
 local orig_number_options=$#
 local except

    # unset option variables to make sure they weren't accidentally
    # exported 
    unset debug stderr_file verbose exceptions

    # If you add new options be sure to change the wildcards below to make
    # sure they are unambiguous (i.e. only match one possible long option)
    # Be sure to show at least one instance of the full long option name to
    # document what the long option is canonically called. 
    # Long options which take arguments will need a `*' appended to the
    # canonical name to match the value appended after the `=' character. 
    while [ $# -gt 0 ]; do
       case z$1 in
          z-D | z--debug | z--d* )
             debug=t
             shift
            ;;
          z-e | z--errors-to* | z--er* )
             get_option_argument maintainer "$1" "$2"
             shift $?

             if [ "${maintainer}" = "maintainer" ]; then
                maintainer="${default_maintainer}"
             fi

             # Redirect all of stderr to a tmp file which we can mail
             # later. 
             stderr_file="/tmp/${progname}.stderr$$"
             exec 2> "${stderr_file}"
            ;;
          z-h* | z--help | z--h* )
             usage
            ;;
          z-t | z--time | z--t* )
             get_option_argument time "$1" "$2"
             shift $?
            ;;
          z-v | z--verbose | z--v* )
             verbose=t
             shift
            ;;
          z-x | z--except | z--ex* )
             unset except
             get_option_argument except "$1" "$2"
             shift $?

             exceptions="${exceptions} ${except}"
            ;;
          z-- )
             shift
             break
            ;;
          z-* )
             usage "${bq}${1}${eq} is not a valid option."
            ;;
          * )
             break
            ;;
       esac
    done

    # Return number of shifted arguments so calling function can shift
    # appropriate amount.
    return $[ orig_number_options - $# ]
}

# Usage: get_option_argument VARIABLE OPTION ARG {OPTIONAL}
#    where VARIABLE is shell variable that will be set to the value ARG.
#    Long option syntax is `--foo=bar' or `--foo bar'.  3rd argument ARG
#    won't get used if first long option syntax was used.  If 4 arg
#    OPTIONAL is non-empty, option isn't required to have an argument; if
#    the argument is missing, VARIABLE is set to the empty value. 
# Returns number of positions caller should shift
function get_option_argument ()
{
 local variable="$1"
 local option="$2"
 local arg="$3"
 local arg_optional="$4"

    # All long options must be at least 3 characters long (--o*), whereas
    # short options are only two chars (-o) and arguments are always
    # separate.
    if [ ${#option} -ge 3 -a "z${option#*=}" != "z${option}" ]; then
       arg="${option#*=}"  # Strip off anything before and including `=' char
       eval ${variable}=\'"${arg}"\'
       return 1
    else
       if [ -z "${arg}" -a -z "${arg_optional}" ]; then
          usage "option ${bq}${option}${eq} requires argument."
       fi
       eval ${variable}=\'"${arg}"\'
       return 2
    fi
}

cleanup_and_exit ()
{
 local exitstat="$?"

   # Reset traps to avoid double execution of this function when a signal
   # is caught (as opposed to normal exit).
   trap '' ${TRAP_SIGNALS}

   rm -rf "${tmpdir}" ${stderr_file} 2> /dev/null

   builtin exit ${exitstat}
}

#
# Time-related functions
#

# Compute the number of seconds specified by a string of the form
#    ...y...M...w...d...h...m...s
# For years, months, weeks, days, hours, minutes, and seconds.  Any given
# unit is optional. 
function timefmt_to_seconds ()
{
 local arg="$*"
 local val
 local unit
 local total
 
    if [ $# -eq 0 ]; then
       read arg
    fi

    shift $#
    set -- $(echo ${arg} | sed 's/\([0-9]*\)\([yMwdhms]\)/\1 \2 /g')

    while [ $# -gt 0 ]; do
       val="${1}"
       unit="${2}"
       shift 2

       # `multiple' is number of seconds per unit (year, month, etc.)
       case "${unit}" in
          y )      multiple=31536000 ;;
          M )      multiple=2592000  ;;
          w )      multiple=604800   ;;
          "" | d ) multiple=86400    ;;
          h )      multiple=3600     ;;
          m )      multiple=60       ;;
          s )      multiple=1        ;;
          * )
             usage "Invalid time+unit argument: ${val}${unit}"
             return 1
            ;;
       esac

       total=$[ total + (val * multiple) ];
    done

    echo "${total}"
}

function seconds_to_days ()
{
    echo $[ ${1:-0} / 86400 ];
}

function seconds_to_minutes ()
{
   echo $[ ${1:-0} / 60];
}

# 
# Routines which search for locks
#

function find_locks ()
{
    verbose_echo "Searching for locks..."

    while read file; do
       verbose_echo "Looking for locks in ${file}"

       lock_info=$(gnatslocks "${file}")

       if [ -n "${lock_info}" ]; then
	  file="`echo ${lock_info%%:*} | sed -e s,/gnats/GNATS/,,g`"
          user="${lock_info#*:\ }"
	  user="${user%%@*}"

          verbose_echo "*** PR ${file} is locked by ${bq}${user}${eq}"

          if user_known_p "${user}" ; then
             echo "	${file}" >> "${tmpdir}/${user}.K"
          else
             owner=$(ls -l "${file}" | awk '{print $3}')
             echo "${file} by ${user}" >> "${tmpdir}/${owner}.U"
          fi
       fi

    done

    verbose_echo "Done searching for locks."
}

# Give some info about each lock.
function gnatslocks ()
{
 local file

    for file in "$@" ; do
       filename=`echo ${file} | sed -e s/\.lock//g`
       echo "${filename}: `cat ${file}`"
    done
}

function user_known_p ()
{
 local file

    for file in /etc/passwd ; do
       if grep -s "^${1}:" "${file}" > /dev/null 2>&1 ; then
          return 0
       fi
    done

    return 1
}

#
# Reporting generating routines
#

function send_reports ()
{
 local file

    pushd "${tmpdir}" > /dev/null

    for file in *.[KU] ; do
       verbose_echo "*** Mailing report to ${file%.[KU]}"
       generate_mail_message "${file}" | sendmail -oi -t
    done

    popd > /dev/null
}

function generate_mail_message ()
{
 local file="$1"
 local recipient="${file%.[KU]}"
 local line
   
    shcat <<- __EOF__
	From: ${progname} (GNATS lock monitor daemon)
	To: ${recipient}
	Bcc: ${maintainer}
	Reply-To: ${maintainer}
	Subject: old GNATS locks
	Precedence: bulk
	
	This is an automated report generated on ${hostname}. 
	
	__EOF__

    case "${file}" in
      "*.K" )
         shcat <<- __EOF__
	You have had some PRs locked for over $(age_echo ${age} ${age_unit}).
	If you no longer need the PR locked, just do \'C-x k\' in the buffer
	to relinquish the lock.  However, if the original emacs session you
	had no longer exists (e.g, emacs crashed, or you did a kill-buffer
	by hand, you have a couple of options.  You can either type:

        /usr/unsupported/lib/gnats/pr-edit --unlock foo/1234

	for each one, or, in emacs, you can do

        M-x unlock-pr RET foo/1234 RET

	to unlock them.  The PRs in question are:
	__EOF__
       ;;
      "*.U" )
         shcat <<- __EOF__
	The following files are presently locked by unknown users, but 
	you (${recipient}) are the present owner of the PR.  These files 
	have been locked over $(age_echo ${age} ${age_unit}).  Please look into this.
	__EOF__
        ;;
    esac

    echo ""
    shcat "${file}"
    echo ""
    shcat <<- __EOF__
	Thanks,
	Your friendly neighborhood GNATS admin.
	__EOF__

}

function age_echo ()
{
 local age="$1"
 local unit="$2"
 
    echo -n "${age} "
    if [ "${age}" = "1" ]; then
       echo "${unit}"
    else
       echo "${unit}s"
    fi
}

function mail_stderr_to_maintainer ()
{
    if [ -z "${stderr_file+set}" ]; then return 0; fi

    if [ -s "${stderr_file}" ]; then 
       sendmail -oi -t <<- __EOF__
	From: ${progname} (GNATS lock monitor daemon)
	To: ${maintainer}
	Subject: ${progname} stderr output
	Precedence: bulk
	
	This is an automated report from host ${hostname}.
	With euid ${EUID} (ruid ${UID}), program "${progname}" ran with 
	the following arguments:
		
	   ${progname_arguments}
		
	and generated the following output on stderr:
		
	$(cat "${stderr_file}")
	__EOF__
    fi
}

function shcat ()
{
 local IFS=""
 local line
 local file
 local exitstat=0
 
    if [ $# -eq 0 ]; then
       while read line; do
          echo "${line}"
       done
       return 0
    else
       for file in "$@" ; do
          if [ -r "${file}" ]; then
             { 
               while read line; do
                  echo "${line}"
               done
             } < "${file}"
          else
             # This will cause the error to be printed on stderr
             < "${file}"
             exitstat=1
          fi
       done 
       return ${exitstat}
    fi
}

function verbose_echo ()
{
    test -n "${verbose+set}" && echo "$@" 1>&2
}

function exit ()
{
    exitstat="$1"
    builtin exit ${exitstat}
}

main "$@"

# 
# eof
#
