#!/bin/bash
# ----------------------------------------------------------------------
# Shell program to test soundmodules.
# Copyright 2008, chirlu <chirlu@gmx.de>.
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
# Revision History:
# Date        Version
# 2008-05-15  0.0.1  File created by new_script ver. 2.1.0
# 2008-05-22  0.0.2  debugged
# 2008-05-25  0.1.0  added options 
# ----------------------------------------------------------------------

########################################################################
# Constants
########################################################################

PROGNAME=$(basename $0)
VERSION="0.1.0"
SND_MODS='ad1816a ad1848 ad1889 adlib ali5451 aloop als100 als300 als4000 aoa aoa-codec-onyx aoa-codec-tas aoa-codec-toonie aoa-fabric-layout aoa-i2sbus aoa-onyx aoa-soundbus aoa-soundbus-i2s aoa-tas aoa-toonie asihpi atiixp atiixp-modem au8810 au8820 au8830 aw2 azt2320 azt3328 bt-sco bt87x ca0106 cmi8330 cmipci cs4231 cs4232 cs4236 cs4270 cs4281 cs46xx darla20 darla24 dt019x dummy echo3g emu10k1 emu10k1-fpga emu10k1x ens1370 ens1371 es1688 es18xx es1938 es1968 es968 fm801 gina20 gina24 gus gusextreme gusmax hda-intel hdsp hdspm hifier ice1712 ice1724 indigo indigodj indigoio intel8x0 intel8x0m interwave interwave-stb korg1212 layla20 layla24 maestro3 mia mixart mona mtpav nm256 opl3-sa2 opti92x-ad1848 opti92x-cs4231 opti93x oxygen pcxhr pdaudiocf powermac rme32 rme96 rme9652 sb16 sb8 sbawe sonicvibes sscape trident usb-audio usb-caiaq usb-usx2y via82xx via82xx-modem virtuoso vx222 vxpocket wavefront ymfpci msnd-pinnacle pdplus sa11xx-uda1341 portman2x4 fm801-tea575x gusclassic harmony loopback miro mpu401 mts64 opl3-synth opl3sa2 cs5535audio cs4231-lib au1x00 ad1848-lib armaaci at91-soc at91-soc-eti-b1-wm8731 opl4-synth pc98-cs4232 seq-dummy serial-u16550 serialmidi sgalaxy soc soc-pcm-sh7760sh7760-ac97 sun-amd7930 sun-cs4231 sun-dbri s3c24xx-soc s3c2410 pcsp pxa2xx-i2sound pxa2xx-soc pxa2xx-soc-corgi pxa2xx-soc-poodle pxa2xx-soc-spitz pxa2xx-soc-tosa riptide rtctimer virmidi cs5530 mpc8610 opti92x opti9xx emu20k1 au8330'

########################################################################
# Functions
########################################################################

function show_usage
{
# ----------------------------------------------------------------------
# Function to display usage message
# ----------------------------------------------------------------------
  echo "Usage: ${PROGNAME} [-t 'MODS'] [-r 'MODS'] [-c] [-a MOD] [-b MOD] [-w NUM]"
} # end of show_usage

function show_help
{
# ----------------------------------------------------------------------
# Function to display help message
# ----------------------------------------------------------------------
  cat << -EOF-

Shell program to test soundmodules
$(show_usage)

Examples: 
$PROGNAME -t 'ad1816a ad1848'   # test snd-ad1816a and snd-ad1848
$PROGNAME                       # test all known soundmodules

Options:
  -h, --help  display this help message and exit
  -l          display list of known soundmodules and exit
  -v          print version information and exit
  -t  'MODS'  test only MODS (not all known modules)
  -r  'MODS'  remove MODS and their referring modules
              before starting the tests
  -c          remove all known soundmodules and their referring modules
              before starting the tests
  -a   MOD    start with testing after MOD in list of soundmodules
  -b   MOD    stop testing before testing MOD
  -w  NUM     wait NUM seconds after adding/removing/using modules
              to ensure that changes have taken full effekt before tests 
              are done or new modules are added/removed (default 1)
  NOTE: You must be the superuser to run this script.
  NOTE: $PROGNAME will only test already installed modules with their
        default configuration (see: man modprobe.conf modprobe modinfo).
        For searching and compiling modules see:
        http://www.alsa-project.org/main/index.php/Matrix:Main
	(use lspci [-v] [-t] to list your PCI devices)
-EOF-
} # end of show_help

function show_list
{
# ----------------------------------------------------------------------
# Function to display list of known soundmodules
# ----------------------------------------------------------------------
  echo ''
  echo 'List of soundmodules taken on 2008-05-16 from:'
  echo 'http://www.alsa-project.org/main/index.php/Matrix:Main'
  echo 'http://www.alsa-project.org/main/index.php?title=Special:Wantedpages'
  echo ''
  echo "$SND_MODS"
} # end of show_list

function show_version
{
# ----------------------------------------------------------------------
# Function to display version information
# ----------------------------------------------------------------------
  echo "${PROGNAME} ${VERSION}"
} # end of show_version

function clean_up
{
# ----------------------------------------------------------------------
# Function to remove temporary files and other housekeeping
# ----------------------------------------------------------------------
  rm -f ${TEMP_FILE}
} # end of clean_up

function error_exit
{
# ----------------------------------------------------------------------
# Function for exit due to fatal program error
#   Argument:
#     1  string containing descriptive error message
# ----------------------------------------------------------------------
  echo "${PROGNAME}: ${1:-"Unknown Error"}" >&2
  clean_up
  exit 1
} # end of error_exit

function graceful_exit
{
# ----------------------------------------------------------------------
# Function called for a graceful exit
# ----------------------------------------------------------------------
  clean_up
  exit
} # end of graceful_exit

function signal_exit
{
# ----------------------------------------------------------------------
# Function to handle termination signals
#   Argument:
#     1  signal_spec
# ----------------------------------------------------------------------
  case $1 in
    INT)  echo "$PROGNAME: Program aborted by user" >&2
      clean_up
      exit
      ;;
    TERM)  echo "$PROGNAME: Program terminated" >&2
      clean_up
      exit
      ;;
    *)  error_exit "$PROGNAME: Terminating on unknown signal"
      ;;
  esac
} # end of signal_exit

function make_temp_files
{
# ----------------------------------------------------------------------
# Function to create temporary files
# ----------------------------------------------------------------------
  # Use user's local tmp directory if it exists
  if [ -d ~/tmp ]; then
    TEMP_DIR=~/tmp
  else
    TEMP_DIR=/tmp
  fi
  # Temp file for this script, using paranoid method of creation to
  # insure that file name is not predictable.  This is for security to
  # avoid "tmp race" attacks.  If more files are needed, create using
  # the same form.
  TEMP_FILE=$(mktemp -q "${TEMP_DIR}/${PROGNAME}.$$.XXXXXX")
  if [ "$TEMP_FILE" = "" ]; then
    error_exit "cannot create temp file!"
  fi
} # end of make_temp_files

function root_check
{
# ----------------------------------------------------------------------
# Function to check if user is root
# ----------------------------------------------------------------------
  if [ "$(id | sed 's/uid=\([0-9]*\).*/\1/')" != "0" ]; then
    error_exit "You must be the superuser to run this script."
  fi
} # end of root_check

function make_backup
{
# ----------------------------------------------------------------------
# Function to make a backup of a file
#     Arguments:
#         1     file to backup (required)
# ----------------------------------------------------------------------
  # Fatal error if required arguments are missing
  if [ "$1" = "" ]; then
    error_exit "make_backup: missing argument 1"
  fi
  # Main function
  if ask_yn "Backup $1 before applying changes?" ; then
    while [ -e "${1}_$(date +'%F_%H-%M-%S').bak" ] ; do
      sleep 1
    done
    local backup_file="${1}_$(date +'%F_%H-%M-%S').bak"
    cp -pv "$1" "$backup_file"
    if ! diff -q "$1" "$backup_file" ; then
      error_exit "make_backup: failed to backup $1"
    fi
  fi
} # end of make_backup

function ask_yn
{
# ----------------------------------------------------------------------
# Function to ask user YES/no - question
#     Arguments:
#         1     question (required)
# ----------------------------------------------------------------------
  # Fatal error if required arguments are missing
  if [ "$1" = "" ]; then
    error_exit "ask_yn: missing argument 1"
  fi
  # Main function
  local yn="_"
  while [ "$yn" = "_" ]; do
    echo -n "$1 [Y/n] "
    read yn
    case $yn in
      ""|y|Y) yn=0
          ;;
      n|N)  yn=1
          ;;
      *)    yn="_"
          echo "Invalid response - please answer y or n"
          ;;
    esac
  done
  return $yn
} # end of ask_yn

function find_in_test_mods
{
# ----------------------------------------------------------------------
# Function to check if $after_mod or $before_mod are in $test_mods
#     Arguments:
#         1     module to find
# ----------------------------------------------------------------------
  if [ "$1" = "" ]; then
    return 0
  fi
  local t_m
  for t_m in $test_mods ; do
    [ "$1" = "$t_m" ] && return 0
  done
  return 1
} # end of find_in_test_mods

function remove_modules
{
# ----------------------------------------------------------------------
# Function to remove soundmodules and their referring modules
#     Arguments:
#         1     list of soundmodules to remove (without 'snd-')
# ----------------------------------------------------------------------
  local r_m
  local i_m
  echo "Removing modules ..."
  for r_m in $1 ; do
    i_m=$(awk "{gsub(\"_\", \"-\", \$1)}; \$1~/^snd-$r_m$/ {print \$1}" /proc/modules)
    if [ -n "$i_m" ] ; then
      modprobe_r_referring "$i_m"
    fi
  done
} # end of remove_modules

function modprobe_r_referring
{
# ----------------------------------------------------------------------
# Function to remove a module and it's referring modules with modprobe -vr
#     Arguments:
#         1     module to remove (full name)
# ----------------------------------------------------------------------
  # Fatal error if required arguments are missing
  if [ "$1" = "" ]; then
    error_exit "modprobe_r_referring: missing argument 1"
  fi
  local referring_modules=$(lsmod | awk "{gsub(\"_\", \"-\", \$1)}; \$1~/^$1\$/ {gsub(\",\", \" \", \$4); gsub(\"_\", \"-\", \$4); print \$4}")
  local r_m
  for r_m in $referring_modules ; do
    modprobe_r_referring "$r_m"
  done
  if modprobe -vr $1 ; then
    sleep $wait_s
  else
    failed_modprobe_r $1
  fi
} # end of modprobe_r_referring

function failed_modprobe_r
{
# ----------------------------------------------------------------------
# Function to handle failed removing with modprobe -vr
#     Arguments:
#         1     module to remove (full name)
# ----------------------------------------------------------------------
  # Fatal error if required arguments are missing
  if [ "$1" = "" ]; then
    error_exit "modprobe_r_referring: missing argument 1"
  fi
  # Main function
  echo -n "Please quit any running audio-application and press <return>"
  read
  sleep $wait_s
  if modprobe -vr $1 ; then
    sleep $wait_s
  else
    local reg_ex=$(echo "$1" | sed -e 's/-/[-|_]/g')
    local etc_mod=$(grep "^\\s*$reg_ex" /etc/modules)
    if [ -n "$etc_mod" ] ; then
      echo "Modules listed in /etc/modules are loaded for all sessions."
      echo "/etc/modules contains $etc_mod."
      if ask_yn "Remove $etc_mod from /etc/modules?" ; then
        make_backup "/etc/modules"
        cp /etc/modules "$TEMP_FILE"
        grep -v "^\\s*$reg_ex" "$TEMP_FILE" > /etc/modules
        echo "Removed $etc_mod from /etc/modules."
        ask_yn "Reboot your system now?" && clean_up && reboot
      fi
    fi
    if ! ask_yn "Continue without removing $1 from the kernel?" ; then
      error_exit "could not remove $1"
    fi
  fi
} # end of failed_modprobe_r

function check_aplay_l
{
# ----------------------------------------------------------------------
# Function to check if "$(aplay -l)" = "" 
# ----------------------------------------------------------------------
  local aplay_l=$(aplay -l 2> $TEMP_FILE)
  if [ -n "$aplay_l" ] ; then
    echo "$aplay_l"
    echo "There are devices listed by aplay -l (see above)."
    echo "It is necessary to remove the responsible soundmodule for running the tests."
    if ask_yn "Try to remove all known soundmodules?" ; then
      remove_modules "$SND_MODS"
      aplay_l=$(aplay -l 2> $TEMP_FILE)
      if [ -n "$aplay_l" ] ; then
        echo "$aplay_l"
        error_exit "aplay -l lists devices -> testing not possible"
      fi
    else
      error_exit "aplay -l lists devices -> testing not possible"
    fi
  fi
} # end of check_aplay_l

function test_modules
{
# ----------------------------------------------------------------------
# Function to test $test_mods
# ----------------------------------------------------------------------
  local here_mods=$(modprobe -l "snd*" | sed -e 's@^.*/@@ ; s@.ko$@@ ; s@_@-@g')
  local t_m
  local aplay_l
  for t_m in $test_mods ; do
    if [ "$t_m" = "$before_mod" ] ; then
      return
    elif [ -n "$after_mod" ] ; then
      [ "$t_m" = "$after_mod" ] && after_mod=""
    elif [ -n "$(echo "$here_mods" | grep "^snd-$t_m\$")" ] ; then
      echo -e "\nTesting snd-$t_m ..."
      modprobe -v snd-$t_m | tee $TEMP_FILE
      sleep $wait_s
      aplay_l=$(aplay -l)
      if [ "$aplay_l" != "" ] ; then
        echo "$aplay_l"
        echo "The soundmodule snd-$t_m maybe works."
        if ask_yn "Test it with totem and alsamixer?" ; then
          totem http://archiv.danceradio.cz:80/archiv/2HOT/2HOT94.ogg & 
          sleep $wait_s
	  alsamixer
          totem --quit
          sleep $wait_s
        fi
        ask_yn "Keep soundmodule snd-$t_m?" && keep_module "$t_m"
      fi
      remove_last_added
      sleep $wait_s
      check_aplay_l
    else
      not_found="$not_found $t_m"
    fi
  done
} # end of test_modules

function remove_last_added
{
# ----------------------------------------------------------------------
# Function to remove the last added modules with rmmod
# ----------------------------------------------------------------------
  local last_added=$(awk '$1~/^insmod$/ {print $2}' $TEMP_FILE)
  local added_reverse=""
  local l_a
  local l_a_short
  local l_a_ins
  for l_a in $last_added ; do
    added_reverse="$l_a $added_reverse"
  done
  for l_a in $added_reverse ; do
    l_a_short=$(echo "$l_a" |  sed -e 's@^.*/\(.*\)\.ko@\1@g ; s@_@-@g')
    l_a_ins=$(awk "{gsub(\"_\", \"-\", \$1)}; \$1~/^$l_a_short\$/ {print \$1}" /proc/modules)
    if [ -n "$l_a_ins" ] ; then
      echo "rmmod $l_a"
      if ! rmmod $l_a ; then
        echo -n "Please quit any running audio-application and press <return>."
        read
        echo "rmmod $l_a"
        if ! rmmod $l_a && ! ask_yn "Continue without removing $l_a?" ; then
          error_exit "could not remove $l_a_short"
	fi
      fi
    fi
  done
} # end of remove_last_added

function keep_module
{
# ----------------------------------------------------------------------
# Function to add a soundmodule to /etc/modules and exit
#     Arguments:
#         1     soundmodule (without 'snd-')
# ----------------------------------------------------------------------
  # Fatal error if required arguments are missing
  if [ "$1" = "" ]; then
    error_exit "keep_module: missing argument 1"
  fi
  # Main function
    echo "Modules listed in /etc/modules should be loaded for all sessions."
    local reg_ex=$(echo "snd-$1" | sed -e 's/-/[-|_]/g')
    if [ -n "$(grep "^\\s*$reg_ex" /etc/modules)" ] ; then
      echo "$(grep "^\\s*$reg_ex" /etc/modules) is already contained in /etc/modules."
    elif [ $(grep -c "^\\s*snd[-|_]" /etc/modules) = 0 ] ; then
      if ask_yn "Add snd-$1 to /etc/modules?" ; then
        make_backup "/etc/modules"
        echo "snd-$1" >> /etc/modules
        echo "Added snd-$1 to /etc/modules."
        ask_yn "Reboot your system now?" && clean_up && reboot
      fi
    elif [ $(grep -c "^\\s*snd[-|_]" /etc/modules) = 1 ] ; then
      local old_snd=$(grep "^\\s*snd[-|_]" /etc/modules)
      if ask_yn "Relace $old_snd with snd-$1 in /etc/modules?" ; then
        make_backup "/etc/modules"
        sed -re "s/^\\s*snd[-|_].*/snd-$1/g" -i /etc/modules
        echo "Replaced $old_snd with snd-$1 in /etc/modules."
        ask_yn "Reboot your system now?" && clean_up && reboot
      elif ask_yn "Add snd-$1 to /etc/modules ?" ; then
        make_backup "/etc/modules"
        echo "snd-$1" >> /etc/modules
        echo "Added snd-$1 to /etc/modules."
        ask_yn "Reboot your system now?" && clean_up && reboot
      fi
    else
      local old_snd=$(grep "^\\s*snd[-|_]" /etc/modules)
      echo "Already in /etc/modules:"
      echo "$old_snd"
      if ask_yn "Add snd-$1 to /etc/modules anyway?" ; then
        make_backup "/etc/modules"
        echo "snd-$1" >> /etc/modules
        echo "Added snd-$1 to /etc/modules."
        ask_yn "Reboot your system now?" && clean_up && reboot
      elif ask_yn "Do you want to edit /etc/modules with vim?" ; then
        make_backup "/etc/modules"
        vim /etc/modules
        ask_yn "Reboot your system now?" && clean_up && reboot
      fi
    fi
  graceful_exit
} # end of keep_module

########################################################################
# Program starts here
########################################################################


##### Initialization And Setup #####
# Set file creation mask so that all files are created with 600 permissions.
umask 066
# Trap TERM, HUP, and INT signals and properly exit
trap "signal_exit TERM" TERM HUP
trap "signal_exit INT"  INT
# Create temporary file(s)
make_temp_files

##### Command Line Processing #####
if [ "$1" = "--help" ]; then
  show_help
  graceful_exit
fi
after_mod=""
before_mod=""
clear_mods=""
rem_mods=""
test_mods="$SND_MODS"
rem_mods=""
wait_s="1"
while getopts ":hlvct:r:w:a:b:" opt; do
  case $opt in
    a )  after_mod=$(echo $OPTARG | sed -e 's/_/-/g; s/\s//g; s/^snd-//g')
         ;;
    b )  before_mod=$(echo $OPTARG | sed -e 's/_/-/g; s/\s//g; s/^snd-//g')
         ;;
    c )  clear_mods="0"
         ;;
    t )  test_mods=$(echo $OPTARG | sed -e 's/_/-/g; s/^snd-//g; s/\ssnd-/ /g')
         ;;
    r )  rem_mods=$(echo $OPTARG | sed -e 's/_/-/g; s/^snd-//g; s/\ssnd-/ /g')
         ;;
    w )  wait_s="$OPTARG"
         if ! [ "$wait_s" -ge "0" ] 2> $TEMP_FILE ; then
           error_exit "${wait_s}: Invalid argument for -w"
         fi
         ;;
    h )  show_help
         graceful_exit ;;
    l )  show_list
         graceful_exit ;;
    v )  show_version
         graceful_exit ;;
    * )  show_usage
         clean_up
         exit 1
         ;;
  esac
done
find_in_test_mods "$after_mod" || error_exit "$after_mod: Argument for -a not in list of soundmodules to test"
find_in_test_mods "$before_mod" || error_exit "$before_mod: Argument for -b not in list of soundmodules to test"

##### Main Logic #####
root_check
[ -n "$clear_mods" ] && remove_modules "$SND_MODS"
[ -n "$rem_mods" ] && remove_modules "$rem_mods"
check_aplay_l
not_found=""
test_modules
[ -n "$not_found" ] && echo -e "\nModules not found:$not_found"
graceful_exit

