CHAPTER 18
User Input Timeout

Sometimes you want a program that accepts user input to run automatically. If the user does not type anything within a specified amount of time, the program should continue running and use a default value for input.

A boot loader is a good example of this type of application. It would give the user a prompt where they will be able to choose the OS or kernel to be booted, but if the user lets the timeout expire, the boot loader uses a previously defined default operating system to boot the system.

An automated system-build script is another example. I wrote one to perform an automated system build while running from a bootable CD. This script would allow the user to choose how to build the system. If there were no response within a predetermined amount of time, the script would continue, using the default build option.

There are several ways to write a script that will time out while waiting for user input, yet continue to run. The first method in this chapter is a brute-force method I devised. It is simple and demonstrates what you can do with multiple processes. The second and third methods are a bit more elegant.

Manual Timeout Method

The code in the following set of three scripts doesn't perform any real action, but it does demonstrate a general framework that can be used to perform timeout-enabled input. The first script was originally a main shell program that prompted the user to decide whether to perform or to skip a specific type of disk partitioning. The main script called two other scripts. The first subsidiary script prompts the user to enter a choice and the second subsidiary script kills the process running the first subsidiary script after a timeout has elapsed, thereby allowing the main script to continue even if no user response is received.

This set of scripts operates as follows: First, the main script invokes the subsidiary killit script to run in the background, where it waits for a set amount of time. After that time period has passed, the killit script wakes up and checks to see if a second process spawned by the main script to read the user's input (readit) is still running. If the readit process is still running, the killit process terminates it. If the readit process does not exist, the killit process exits quietly. In either case, the main script continues with other tasks after the readit process has terminated.

The following code is a template for the initiating script, called buildit. It calls the two helper scripts (killit and readit), and its purpose is to determine and display the return code from the readit script. In a genuine application (as opposed to our template), the main script would be able to establish its next course of action based on that return code.

#!/bin/sh
HOMEDIR=$HOME/scripts
$HOMEDIR/killit &
$HOMEDIR/readit
ans=$?
echo The return code is: $ans

Next, is a template for a readit script. This template displays the chosen timeout value and asks for input from the user. It requires only a simple yes or no answer, but it could just as easily accept a more complex question with more than two possible answers. Once the user has given a response, the script exits with the appropriate return code.

#!/bin/sh
echo Timeout in 3 seconds...
echo -e "Do you want to skip or not? (y and n are valid):"
read ans
ans=`echo $ans | tr "[A-Z]" "[a-z]"`
if [ "$ans" = "y" ]
then
  exit 1
else
  exit 2
fi

Finally, the following is an example of a killit script. It puts itself to sleep for the predetermined timeout period (three seconds) and then checks the process table for a running readit script. If the readit script is found, the killit script assumes after awakening that the user hasn't answered the question from the readit script yet and that the readit script has waited long enough. The killit script then kills the readit process, thereby allowing the calling script, buildit, to continue.

#!/bin/sh
sleep 3
readit_pid=`ps -ef | grep readit | grep -v grep | awk '{print $2}'`
if [ "$readit_pid" != "" ]
then
  kill $readit_pid
fi

There are a couple items to note when using this method of timing out while waiting for user input. First, the return code returned by the buildit script may be a value other than what is defined in the readit code, and probably won't be obvious. When the readit script terminates normally, after the user enters an appropriate value, the return code displayed by the buildit script will be either 1 or 2. However, if the killit script kills the readit script, the readit script does not generate a return code. The shell recognizes that a process has been terminated and it assigns to that process a return value that is the sum of a specified value and the kill signal that was used to kill it. The specified value returned by the buildit script depends on the shell. The specified value of a process terminated in bash is 128 plus the terminating signal value, whereas in ksh it is 256. Assuming bash for our example, one of the most common signals for terminating a process would be 15 (or SIGTERM) and the return code would be 143 (the sum of 128 and 15). If the "kill it no matter what" signal of 9 (SIGKILL) were used, the return code would be 137. There are many different terminating signals; these are just two of the most common.

Second, when the readit process is killed, it generates a message that is sent to the stderr (standard error) I/O stream of the main buildit script, stating that the process was killed. If you don't want to see that message, you will have to deal with that output, for example by redirecting the stderr of the buildit script to a file or to /dev/null.

Timeout Using stty

The second method of handling a user timeout is based on some cool features of stty. It is also more elegant, as you don't need to write several scripts or spawn jobs that run in the background. The stty command lets you list and modify line settings of your terminal. It can be used to define various keystrokes such as ^+C or Backspace. We will use the stty command's min and time settings. Both must be used with the -icanon switch, which disables canonical-mode input processing. In this mode, normal input that usually is read as a whole line is disabled and input bytes are then controlled by the min and time settings.

#!/bin/sh
/usr/bin/echo -n "Input a letter or wait 3 seconds: "
stty -icanon min 0 time 30

The min value is the minimum number of characters for a complete read of user-requested input. We set the value to 0 so the read will always be complete, even if there is no input from the user. The time value is the timeout measured in tenths of a second. This is much more fine-grained than the earlier example code that used full seconds to measure time using the sleep command.

Once you've set the stty values, the script uses the dd command as shown in the following code to receive input from the user and to save that input (if the input is received within the timeout period) in the ANSWER variable:

ANSWER=`dd bs=1 count=1 2>/dev/null`
stty icanon
echo ; echo Answer: $ANSWER

In this case, only a single character will be received. If user input is to exceed one character, you would need to increase the min value of stty and modify the count value of the dd command to match the required input.

Here the stty settings are reset to normal and the answer is displayed. However, in a "real" application script, the presence or absence of an answer from the user would have to be tested and handled by subsequent code.

In current versions of both the ksh and bash shells, the built-in read command has a timeout option (-t seconds). This takes all the difficulty out of user input.

General Timeout Utility

The final utility1 in this chapter is much like the design of the manual method discussed previously, but is much simpler and is self-contained. Instead of requiring three separate scripts, this single function handles all the work. It is also not specific to user-input applications. It can be used for any type of command to which you may want a timeout value applied. Since this is a self-contained utility, it is a good candidate for addition to a shell library discussed in Chapter 2.

I recently used this method for setting a timeout value within a system monitor that attempts an ssh to a remote machine. In some cases a system will seem to be alive based on a ping result, but an attempted connection to the machine will hang forever. This is where the timeout ability is required.

The first half of the function sets the timeout value and the command that is received from the function call. It then runs the command in the background and determines the process ID of that backgrounded task.

timeout()
{
  waitfor=5
  command=$*
  $command &
  commandpid=$!

The second half is where the cleverness lies. First the function sends a combination of two commands to the background. The first command is a sleep that delays for the specified amount of time. The second command kills the original process after the sleep completes.

  (sleep $waitfor ; kill −9 $commandpid >/dev/null 2>&1) &
  watchdogpid=$!
  sleeppid=`ps $ppid $watchdogpid | awk '{print $1}'`
  wait $commandpid
  kill $sleeppid >/dev/null 2>&1
}

__________

1. This script is based on an idea by Heiner Stevens. You can find the original implementation at http://shelldorado.com/scripts/cmds/timeout.txt/.

Once this combination of commands is backgrounded, the background process ID is determined. The function then waits for the original backgrounded process to complete, whether it was killed or not. If the original backgrounded process completes normally and doesn't need to be killed, the function kills the backgrounded watchdog process.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.104.35