|
February 2008 AAuSE, Take
2 |
Copyright (C) 2008 by Steve Litt. All rights reserved. Materials from guest authors copyrighted by them and licensed for perpetual use to Linux Productivity Magazine. All rights reserved to the copyright holder, except for items specifically marked otherwise (certain free software source code, GNU/GPL, etc.). All material herein provided "As-Is". User assumes all risk and responsibility for any outcome.
|
Twenty Eight Tales of Troubleshooting Troubleshooting: Just the Facts Manager's Guide to Technical Troubleshooting Troubleshooting Techniques of the Successful Technologist |
[ Troubleshooters.Com
| Back Issues |Troubleshooting
Professional Magazine
]
|
|
CONTENTS
|
Author's Note
This article was written when I understood much less about shellscripting. As a result, some of my assertions, such as the article's assertion that finding a process ID isn't quite as easy as it might seem, aren't true. Much of this article is contradicted later in the magazine. That's OK, as I said, this was a journey, and be assured that whatever the article says, the code in this article does work quite nicely. |
forever foreach line in the playlist file timcommand = "timidity " + song_filename system(timcommand)The preceding is wonderful with one exception. The user must interact with the playing. The user might want to go to the next or previous song, start this one over, quit, or jump to a specific song. So the program must continuously read the incoming command FIFO, and do the bidding of the commands coming in through that FIFO. Still no problem:
forever check FIFO and read if necessary if FIFO contains command if command = quit break else adjust playlist subscript according to command endif else increment playlist subscript, or set back to 1 if already on last song endif play song pointed to playlist subscriptThat was easy, wasn't it! Can you see the problem?
childpid=$(cat ./child.pid) timpid=$(ps -aH | grep -A1 ^[[:space:]]*$childpid | tail -n1 | cut -b1-5)The following scripts provide a proof of concept for this way a shellscript can identify the PID used by timidity:
| parent.sh | child.sh | |
#!/bin/bash echo $$ > parent.pid ./child.sh & sleep 1 childpid=$(cat ./child.pid) timpid=$(ps -aH | grep -A1 ^[[:space:]]*$childpid | tail -n1 | cut -b1-5) echo child=$childpid echo timidity=$timpid echo ps ax | tail -n8 echo echo -n 'Kill it now? (y or n)==>' read killflag if test "$killflag" = "y"; then kill $timpid kill $childpid fi |
#!/bin/bash echo $$ > child.pid /usr/bin/timidity test.mid echo finished > /tmp/steve.fifo echo FINISHED FINISHED FINISHED |
#!/usr/bin/ruby
def play()
pid = fork
if pid == nil
### THIS IS THE CHILD
pid2 = fork
if pid2 == nil
### THIS IS THE GRANDCHILD
### REPLACE IT WITH MIDI PLAYER
Kernel.exec("timidity", @filename.to_s)
else
### THE CHILD
### ITS PURPOSE IS TO
### FORK OFF TIMIDITY AND
### WRITE finished TO FIFO
### WHEN SONG IS FINISHED
print "dia pid grandchild=" + pid2.to_s + "\n"
pid2file = File.new("/tmp/pid2.pid", "w");
pid2file.puts(pid2.to_s);
pid2file.close()
@returnstring = "finished"
Process.wait(pid2)
### RETURN PROPER STRING THRU FIFO
writebackfile = File.new("/d/bats/littmidi.fifo", "w");
writebackfile.puts(returnstring);
writebackfile.close()
puts "dia returnstring is #{returnstring}"
Process.exit(0)
end
else
### THIS IS THE PARENT
pidfile = File.new("/tmp/pid.pid", "w");
pidfile.write(pid.to_s)
pidfile.close()
Process.detach(pid)
puts "dia pid child=#{pid}"
end
end
|
def superkill
pidfile = File.new("/tmp/pid.pid", "r");
pid = pidfile.readline().to_i;
pidfile.close()
Process.kill("HUP", pid)
pidfile = File.new("/tmp/pid2.pid", "r");
pid2 = pidfile.readline().to_i;
pidfile.close()
Process.kill("HUP", pid2)
end
def kill
self.superkill unless @paused
end
|
def getNextLine(infile) while infile.eof sleep(0.5) end line = infile.gets line.chomp! sleep(0.5) return line end |
![]() |
Block
diagram of my
timidity front end. Major components are UMENU menu and the frontmidi.rb
program, which handles playlist functions and interprocess
communication between user commands coming through the FIFO, the
playlist, and the processes running timidity. Other significant
components are the filepicker and recordpicker, aumix and timidity. frontmidi.rb's sole connection to the user is the commands coming through the fifo. For debugging purposes the programmer can input such commands directly by redirecting text to the fifo. UMENU is responsible for converting user menu choices to commands useable by frontmidi.rb, and sending them down the FIFO. The song object also writes the FIFO on completion of a song it writes "finished" to the FIFO. |
#!/usr/bin/ruby -w
# =======================================================================
#
# frontmidi.rb, version 0.1.0, copyright (C) 2008 by Steve Litt
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# 3. The name of the author may not be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# =======================================================================
#
# frontmidi.rb, version 0.1.0 1/30/2008
# This version is somewhat unstable and has been only lightly tested.
# BUGS:
# * Throws useless error messages on uncaught SIGHUP signal to child process
# * Aborts if quickly repeated next commands go past an
# unplayable song
# * The fifo file is housed in a directory you might not have. Either
# change all instances of the fifo file (and change them in UMENU),
# or create /d/bats/littmidi.fifo.
#
# =======================================================================
def fifo()
"/d/bats/littmidi.fifo"
end
class Playlist
attr_accessor :paused
attr_reader :fname
attr_reader :songs
attr_reader :numsongs
attr_accessor :songnumber
attr_reader :songname
def initialize(fname)
paused = false
@fname = fname
@songs = []
@numsongs = 0
playlistfile = File.new(fname, "r");
playlistfile.each do
|line|
line.chomp!
skip = 1
if line =~ /\.mid\s*$/
skip = 0
end
if line =~ /^\s*$/
skip = 1
end
if line =~ /^\s*\#/
skip = 1
end
if skip == 0
@songs.push([line])
@numsongs += 1
end
end
playlistfile.close()
@songnumber = 1
@songname = @songs[@songnumber-1]
end
def set(num)
if(num < 1)
@songnumber=1
elsif(num > @numsongs)
@songnumber = @numsongs
else
@songnumber = num
end
@songname = @songs[@songnumber-1]
end
def next
@songnumber += 1
if @songnumber > @numsongs
@songnumber = 1
end
@songname = @songs[@songnumber-1]
end
def prev
@songnumber -= 1
if @songnumber < 1
@songnumber = @numsongs
end
@songname = @songs[@songnumber-1]
end
def write2stdout()
puts "There are #{@numsongs.to_s} songs in this playlist:"
ss1 = 1
@songs.each do
|song|
puts "#{ss1.to_s}: #{song}"
ss1 += 1
end
end
end
class Midisong
attr_reader :returnstring
attr_reader :filename
attr_accessor :paused
def initialize(filename)
puts "dia top Midisong init filename=#{filename}"
@filename = filename
@paused = false
end
def superkill
pidfile = File.new("/tmp/pid.pid", "r");
pid = pidfile.readline().to_i;
pidfile.close()
Process.kill("HUP", pid)
pidfile = File.new("/tmp/pid2.pid", "r");
pid2 = pidfile.readline().to_i;
pidfile.close()
Process.kill("HUP", pid2)
end
def kill
self.superkill unless @paused
end
def play
pid = fork
if pid == nil
### THIS IS THE CHILD
pid2 = fork
if pid2 == nil
### THIS IS THE GRANDCHILD
### REPLACE IT WITH MIDI PLAYER
Kernel.exec("timidity", @filename.to_s)
else
### THE CHILD
### ITS PURPOSE IS TO
### FORK OFF TIMIDITY AND
### WRITE finished TO FIFO
### WHEN SONG IS FINISHED
print "dia pid grandchild=" + pid2.to_s + "\n"
pid2file = File.new("/tmp/pid2.pid", "w");
pid2file.puts(pid2.to_s);
pid2file.close()
@returnstring = "finished"
Process.wait(pid2)
### RETURN PROPER STRING THRU FIFO
writebackfile = File.new(fifo, "w");
writebackfile.puts(returnstring);
writebackfile.close()
puts "dia returnstring is #{returnstring}"
Process.exit(0)
end
else
### THIS IS THE PARENT
pidfile = File.new("/tmp/pid.pid", "w");
pidfile.write(pid.to_s)
pidfile.close()
Process.detach(pid)
puts "dia pid child=#{pid}"
end
end
end
def getNextLine(infile)
while infile.eof
sleep(0.5)
end
line = infile.gets
line.chomp!
sleep(0.5)
return line
end
puts "Starting"
puts "dia pid parent=#{Process.pid()}"
playlist=Playlist.new(ARGV[0])
song = Midisong.new(playlist.songname)
song.play()
commandfile = File.new(fifo, "r")
while true
command = getNextLine(commandfile)
if song.paused
puts "Unpausing. Now perform the desired command!"
command = "unpause"
end
puts command
case command
when 'finished'
playlist.next()
song = Midisong.new(playlist.songname)
song.play
when 'next'
song.kill()
playlist.next()
song = Midisong.new(playlist.songname)
song.play
when 'prev'
song.kill()
playlist.prev()
song = Midisong.new(playlist.songname)
song.play
when 'firstsong'
song.kill()
playlist.set(1)
song = Midisong.new(playlist.songname)
song.play
when 'lastsong'
song.kill()
playlist.set(playlist.numsongs)
song = Midisong.new(playlist.songname)
song.play
when 'beginsong'
song.kill()
song = Midisong.new(playlist.songname)
song.play
when 'pause'
song.kill()
song.paused = true
when 'unpause'
if song.paused
song = Midisong.new(playlist.songname)
song.play
end
when /^playlist\s/
temp=command
temp.gsub!(/playlist\s*/){""}
temp.gsub!(/\s.*/){""}
puts "dia playlist=#{temp}"
playlist=Playlist.new(temp)
song.kill()
song = Midisong.new(playlist.songname)
song.play
when /^specificsong \d/
temp=command
temp.gsub!(/specificsong\s*/){""}
temp.gsub!(/\D.*/){""}
puts "dia specificsong=#{temp}"
song.kill()
playlist.set(temp.to_i)
song = Midisong.new(playlist.songname)
song.play
when 'quit'
song.kill()
break
end
end
print "Goodbye\n\n"
|
Ruby program Open source license Bugs The fifo file. Must match the fifo defined in UMENU The playlist object handles all playlist functionality Playlist file loop. This defines which playlist entries are legitimate. To be legitimate, an entry must end in .mid, must not be blank, and must not start with a comment char (#). This definition MUST match UMENU'S definition, or the "jump to song" functionality might play the wrong song. Song handler object superkill method kills the child and grandchild kill method Don't kill descendents if paused play method creates child and grandchild grandchild is replaced with timidity Replace grandchild with timidity Save grandchild pid Child waits for completion or termination of grandchild, which is timidity Tell main process song is finished Child process terminates itself after completion or termination of grandchild Save child pid to file Detach child from main process, meaning main and child proceed independently Function to acquire commands from the fifo. Eof loop waits for input. Second sleep necessary to prevent crashing in the face of rapid, repetitive commands from fifo Initialize playlist object Initialize playlist's first song Start the song Open the fifo for input Command handling loop When player is paused, the next command is disabled other than clearing the pause flag. Normal song repetition. Child and grandchild already completed and terminated. Just incriment playlist and play the song. Next. Kill current child and grandchild (timidity), increment playlist and play. Previous. Kill current child and grandchild (timidity), increment playlist and play. Rewind to playlist's first song. Kill current child and grandchild (timidity), set playlist to first song, and play. Proceed to playlist's last song. Kill current child and grandchild (timidity), set playlist to last song, and play. Replay current song from its beginning. Leave playlist untouched, kill current child and grandchild (timidity), and play. Pause. Kill child and grandchild, and set paused flag. Unpause. Resume play. Code at top of loop already cleared pause flag. Change playlist. Playlist's filename follows the "playlist" keyword. Kill child and grandchild and play new playlist's first song. Choice of the playlist is done externally to this program (typically from UMENU). Jump to song. Song's 1 based number follows keyword "specificsong". Kill child and grandchild, set playlist to new song number, and play playlist's new current song. Actual choice of song is done externally, typically from UMENU. UMENU's definition of legitimate songs on list must match this program's. Quit the program. Kill child and grandchild, terminate the command loop, and fall through to program's end. |
ZZM:::Litt's Text Midi Frontender pAuse param C: echo pause > /d/bats/littmidi.fifo Unpause param C: echo unpause > /d/bats/littmidi.fifo; Beginning of song param C: echo beginsong > /d/bats/littmidi.fifo Next param C: echo next > /d/bats/littmidi.fifo Previous param C: echo prev > /d/bats/littmidi.fifo First song param C: echo firstsong > /d/bats/littmidi.fifo Last song param C: echo lastsong > /d/bats/littmidi.fifo Jump to song param D: /scratch/oldsongs/mid C: echo dia1; C: FN=$(persist playlist=? $HOME/littmidi.state); C: TMPFILE=`mktemp`; C: echo dia2; C: persist action= $HOME/littmidi.state; C: persist recno= $HOME/littmidi.state; C: echo dia3; C: echo "superselect=n" >> $TMPFILE; C: echo "startofdata" >> $TMPFILE; C: echo dia4; C: echo dia4.5 FN=$FN; C: cat $FN >> $TMPFILE; C: echo dia5; C: cat $FN | grep "\.mid\s*$" | grep -v "^\s*$" | C: grep -v "^\s*#" >> $TMPFILE; C: echo dia6; C: /d/at/cpp/filepicker/rpick $TMPFILE; C: echo dia7; C: cat $TMPFILE; C: grep "action=" $TMPFILE >> $HOME/littmidi.state; C: ACTION=$(persist action=? $HOME/littmidi.state); C: persist action= $HOME/littmidi.state; C: if grep -q "action=select" $TMPFILE; then C: grep "recno=" $TMPFILE >> $HOME/littmidi.state; C: RECNO=$(persist recno=? $HOME/littmidi.state); C: persist recno= $HOME/littmidi.state; C: let RECNO=RECNO+1; C: echo specificsong $RECNO > /d/bats/littmidi.fifo; C: fi; C: rm -f $TMPFILE; Run frontmidi.rb param D: /scratch/oldsongs/mid C: TMPFILE=`mktemp`; C: echo "dir=/scratch/oldsongs/mid/lists/" > $TMPFILE; C: /d/at/cpp/filepicker/fpick $TMPFILE; C: if grep -q "action=select" $TMPFILE; then ACTION=SELECT;fi; C: if test "$ACTION" = "SELECT"; then C: FN=`grep "^file=" $TMPFILE | sed -e "s/.*=//"`; C: persist "playlist=$FN" $HOME/littmidi.state; C: /d/at/ruby/frontmidi.rb $FN; C: fi; C: rm -f $TMPFILE; S: 1 Change Playlist param D: /scratch/oldsongs/mid/lists C: TMPFILE=`mktemp`; C: echo "dir=/scratch/oldsongs/mid/lists/" > $TMPFILE; C: /d/at/cpp/filepicker/fpick $TMPFILE; C: if grep -q "action=select" $TMPFILE; then ACTION=SELECT;fi; C: if test "$ACTION" = "SELECT"; then C: FN=`grep "^file=" $TMPFILE | sed -e "s/.*=//"`; C: persist "playlist=$FN" $HOME/littmidi.state; C: echo playlist $FN > /d/bats/littmidi.fifo; C: fi; cHange Playlist param D: /scratch/oldsongs/mid/lists C: TMPFILE=`mktemp`; C: echo "dir=/scratch/oldsongs/mid/lists/" > $TMPFILE; C: /d/at/cpp/filepicker/fpick $TMPFILE; C: if grep -q "action=select" $TMPFILE; then ACTION=SELECT;fi; C: if test "$ACTION" = "SELECT"; then C: FN=`grep "^file=" $TMPFILE | sed -e "s/.*=//"`; C: persist "playlist=$FN" $HOME/littmidi.state; C: echo quit > /d/bats/littmidi.fifo; C: echo Wait two seconds... C: sleep 2; C: /d/at/ruby/frontmidi.rb $FN; C: fi; C: rm -f $TMPFILE Kill Littmidi.rb param C: echo quit > /d/bats/littmidi.fifo Zap zombie midi players param C: zapmidizombies S: 1 Volume ::: Mplayer volume Louder param C: VOLM=$(persist "volume=?" $HOME/littmidi.state); C: if test "$VOLM" = ""; then C: let VOLM=60; C: persist "volume=$VOLM" $HOME/littmidi.state; C: fi; C: if test $VOLM -lt 100; then C: let VOLM=$VOLM+1; C: persist "volume=$VOLM" $HOME/littmidi.state; C: aumix -v $VOLM -w $VOLM; C: fi; C: echo "Volume is $VOLM"; Softer param C: VOLM=$(persist "volume=?" $HOME/littmidi.state); C: if test "$VOLM" = ""; then C: let VOLM=60; C: persist "volume=$VOLM" $HOME/littmidi.state; C: fi; C: if test $VOLM -gt 0; then C: let VOLM=$VOLM-1; C: persist "volume=$VOLM" $HOME/littmidi.state; C: aumix -v $VOLM -w $VOLM; C: fi; C: echo "Volume is $VOLM"; Mute param C: aumix -v 0 -w 0; Unmute param C: VOLM=$(persist "volume=?" $HOME/littmidi.state); C: if test "$VOLM" = ""; then C: let VOLM=60; C: persist "volume=$VOLM" $HOME/littmidi.state; C: fi; C: aumix -v $VOLM -w $VOLM; C: echo "Volume is $VOLM"; Observe Volume param C: VOLM=$(persist "volume=?" $HOME/littmidi.state); C: echo Littmidi volume=$VOLM; C: aumix -q; S: 1 seT volume to specific number (0-100) param C: ORG=$(persist "volume=?" $HOME/littmidi.state); C: VOLM=%1%Volume please (0-100)%%; C: echo "Original volume=$ORG"; C: echo "$VOLM" | grep "[^[:digit:]]"; C: NOTANUMBER=$?; C: test -z $VOLM; C: ZEROLENGTH=$?; C: if test "$NOTANUMBER" = "1" -a "$ZEROLENGTH" = "1"; then C: persist "volume=$VOLM" $HOME/littmidi.state; C: aumix -v $VOLM -w $VOLM; C: echo "New volume is $VOLM"; C: else C: echo "Bad input: $VOLM"; C: echo "Volume unchanged at $ORG"; C: fi; S: 1 ^Quit Special functions ::: Special Functions Menu Drain FIFO param C: cat /d/bats/littmidi.fifo S: 1 Ps ax param C: TEMP=`ps ax | grep frontmidi.rb`; FIFO Tasks ::: FIFO Task Menu Delete current FIFO param C: rm -f /d/bats/littmidi.fifo; C: ls -l /d/bats/littmidi.fifo S: 1 Create FIFO param C: mkfifo /d/bats/littmidi.fifo; C: ls -l /d/bats/littmidi.fifo S: 1 ^Quit What time is it? param C: date +%Y/%m/%d__%H:%M:%S S: 1 Edit Litt's Text Music Player menu param C: gvim /d/bats/zzm.emdl Rebuild Litt's Text Music Player menu param C: /d/bats/rebuild_umenu_zzm.sh ^Quit plaYlist ::: Playlist menu Get Current Playlist param C: FN=$(persist playlist=? $HOME/littmidi.state); C: echo $FN S: 1 Edit Playlist param C: gvim $(persist playlist=? $HOME/littmidi.state); ^Quit ^eXit |
Retrieve current playlist Make tempfile for recordpicker erase former persistent action and record number cannot select whole directories start of data flag to recordpicker Put playlist file lines into record picker, using same criteria as frontmidi.rb. Note lack of semicolon means the line after the cat command is appended to the end of the cat command. Find user action If use SELECTed, get the record number, remember it, and send its one-based equivalent as a specificsong command to frontmidi.rb. Make temp file to communicate with filepicker. Put starting directory into temp file. Run record picker. If user selected, get the chosen filename and start frontmidi.rb with that filename as ARG0. Same as "Run frontmidi.rb", except send the filename down the fifo with a playlist command. Depricated. Change playlists by stopping and restarting frontmidi.rb. The zapmidizombies shellscript does a killall on timidity, and greps ps ax to obtain pids of all frontmidi.rb instances, both parents and children, and issues kill commands for them. Get the single-value volume number from storage. If it's not present, set it to 60 and save it. Increment the volume number if less than 100, and save it. Set aumix master volume AND PCM volume to the single-value number. Crude, but it works well. Just like "Louder", except decrements the volume number if greater than zero. Sets audio volumes to zero, but does not store or change the front end volume number Retrieves the stored front end volume number, and sets the aumix master and pcm volumes to that number. Shows the front end single value number followed by all aumix values. The S: 1 at the end prevents a new menu from overwriting the output until the user presses a key. Sets the front end single-value number to the user's input. Retrieves the current number, queries for and stores the user's input. It checks for non-numerics and for a zero length string, and issues an error message if either is true. Otherwise it changes the single value number, saves it, and sets aumix's pcm and master volumes to that number. Error messages on bad input Stops for user to read messages, until user acknowledges with a keystroke. Edit the EMDL file that is the source for the menu. Compiles the source EMDL into a menu system. |
![]() |
Here's the main menu. The top choice is to run the ruby program. Pause, unpause, beginning of song, first song, next, previous, and last song send commands to the ruby program through the FIFO. Change playlist and Jump to song first give the user a list to choose from, and then submit the appropriate command through the FIFO. The Kill Littmidi.rb submits a quit command through the FIFO, and in response the Ruby program ends. The Zap zombie choice uses kill commands to strongarm all the ruby midiplayer programs. The choices with elipses in front of them bring up submenus. Exit exits the menu, but does not terminate the Ruby program. |
![]() |
Here's
the volume
submenu. Louder increases the volume a small amount and can be pressed
repeatedly. Softer does the same thing but decreases the volume. Mute
sets the volume to zero but remembers the former volume. Unmute brings
back the former volume. Observe volume looks at persistent storage to
tell you the current setting. Set volume to specific number queries the
user for a volume number, stores it persistently, and sets the volume
accordingly. All these commands raise and lower volume by calling aumix, and do not submit commands to the Ruby program. |
![]() |
The Special Functions submenu. CLI Run littmidi.rb runs the Ruby program in the terminal now used by the menu. Drain FIFO does what it says, and is used for maintenance and diagnostics when things go wrong. Ps ax shows the process list. FIFO tasks is a submenu. What time is it shows the current time, Edit Litt's Text Music Player menu lets you edit the EMDL file that created the menu, and Rebuild Litt's Text Music Player menu compiles the EMDL file, and after displaying any errors, enables the user to transfer the newly compiled menu files to UMENU. |
![]() |
Here's the FIFO task menu. It enables you to delete or create the FIFO. This is used only for troubleshooting when things go very wrong. |
![]() |
The Playlist submenu. Here you can edit the current playlist (in Vim) or see what the current playlist's filename is. |
![]() |
This month I proved that concept! |
| Parent program | spawner.sh | |
#!/bin/bash ### DEFINE BINARY COMMAND TO SPAWN, AND COMMAND TO RUN ON ITS TERMINATION SPAWNEDCOMMAND='sleep 20' FINISHEDCOMMAND="./test.sh $TMPFILE" ### BUILD THE TEMPORARY FILE TO COMMUNICATE WITH THE CHILD TMPFILE=mktemp echo $SPAWNEDCOMMAND > $TMPFILE echo $FINISHEDCOMMAND >> $TMPFILE ### RUN THE SPAWNER, IN THE BACKGROUND, WITH THE TEMP FILE AS ARG1 ./spawn.sh $TMPFILE & ### DEDUCE CHILD AND GRANDCHILD PIDs MYPID=$$ CHILDPID=$(ps -aH | grep -A1 "^\s*$MYPID" | tail -n1 | cut -b1-5) GRANDCHILDPID=$(ps -aH | grep -A1 "^\s*$CHILDPID" | tail -n1 | cut -b1-5) ### APPLICATION CODE GOES BELOW RESPONSE='n' #echo -n Do you want to kill: $SPAWNEDCOMMAND \(y, n\)? ==\> read RESPONSE if test "$RESPONSE" = "y"; then # kill $CHILDPID # UNCOMMENT TO PREVENT FINISHEDCOMMAND FROM RUNNING kill $GRANDCHILDPID fi echo echo tempfile follows cat $TMPFILE echo =============== sleep 10 echo tempfile follows cat $TMPFILE |
#!/bin/bash TEMPFILE=$1 SPAWNEDCOMMAND=$(head -n1 $TEMPFILE) FINISHEDCOMMAND=$(head -n2 $TEMPFILE | tail -n1) $SPAWNEDCOMMAND $FINISHEDCOMMAND |
#/bin/bash
function on_hup ()
{
kill -s SIGHUP $COMMANDPID
kill -s SIGUSR2 $PARENTPID
exit 1
}
PARENTPID=$PPID
THISPID=$$
COMMAND2SPAWN=$1
trap on_hup SIGHUP
$COMMAND2SPAWN &
COMMANDPID=$!
echo Parent PID=$PARENTPID
echo This PID=$THISPID
echo CommandPID=$COMMANDPID
wait $COMMANDPID
kill -s SIGUSR1 $PARENTPID
exit 0
|
Check out spawn.sh at the
left. It traps SIGHUP with function on_hup().
It records the parent PID, which it will use later to contact the
parent as to whether it completed or was HUPped. The first and
only argument is the command to get spawned. It runs that command in
the background, grabs the spawned command's PID, and waits on that PID. If someone HUPs spawn.sh, function on_hup() executes, killing the spawned command, sending a USR2 message to the parent that called spawn.sh, and exits. However, if the spawned command terminates normally, execution falls through the wait, and spawn.sh sends USR1 message to the parent that called spawn.sh, and then the program exits. In summary, it runs the spawned program in the background. If it gets HUPped, it HUPs the spawned background program and sends USR2 to the calling program. Otherwise, the spawned background program completes and USR1 is sent to the calling program. |
#!/bin/bash
### HANDLE THE SONG FINISHING NORMALLY
function on_usr1 () {
echo "Song ended normally."
kill -s SIGHUP $sleeppid
}
# HANDLE THE SONG GETTING HUPped
function on_usr2 () {
echo "Process was killed!"
kill -s SIGHUP $sleeppid
exit 1
}
### PROCESSING STARTS HERE
### SET SIGUSR1 AND SIGUSR2 TRAPS
trap on_usr1 SIGUSR1
trap on_usr2 SIGUSR2
### RUN THE SONG IN THE BACKGROUND
./spawn.sh "timidity ./test.mid" &
### GET THE BACKGROUND PROCESS'S PID
spawnpid=$!
echo $spawnpid > ./spawnpid.pid
### SLEEP FOREVER, UNLESS A SIGNAL COMES THROUGH
sleep 1000000 &
### GRAB THE PID OF SLEEP SO YOU CAN KILL IT TO GO FORWARD
sleeppid=$!
### WAIT FOR THE SLEEP TO END
wait $sleepid
echo "Fell through after the song completed normally"
|
Start by creating
functions to handle signals USR1 and USR2, and trap them. Then run spawn.sh to
spawn timidity. It then writes out spawner.sh's PID to file ./spawnpid.pid, and sleeps forever. It will not proceed until that sleep command terminates, which will happen only if the sleep command is killed. The handler functions for USR1 and USR2 both kill that sleep. One more thing: For some reason, if you perform a ps command, spawner.sh will not appear. Instead, it will appear with the same name as the script that called it. |
#!/bin/bash
function on_usr1 ()
{
echo "Song ended normally."
./increment_songno.sh
persist "spawnpid=999999" $HOME/littmidi_b.state
kill -s SIGHUP $sleeppid
}
function on_usr2 ()
{
echo "Process was killed!"
persist "spawnpid=999999" $HOME/littmidi_b.state
kill -s SIGHUP $sleeppid
exit 1
}
trap on_usr1 SIGUSR1
trap on_usr2 SIGUSR2
while true; do
songno=$(persist "songno=?" $HOME/littmidi_b.state)
songname=$(./songnum2name.sh $songno)
echo songname=$songname
command="timidity $songname > /dev/null"
echo Command=$command
./spawn.sh "$command" &
spawnpid=$!
persist "spawnpid=$spawnpid" $HOME/littmidi_b.state
### Next line sleeps forever, relies on a signal
### to kill it.
sleep 1000000 &
sleeppid=$!
wait $sleepid
done
|
As before, USR1 and
USR2 are
trapped and referred to functions. Both functions kill the forever
sleep. USR1 comes in when the song runs its course, while USR2 comes in
when the song (spawner.sh
actually) gets HUPped. On USR1 the playlist is incremented, while on
USR2 it's not. Each iteration of main loop gets the song number, spawn timidity on that song, persistently records the PID for spawn.sh so it can be killed from another process, and sleeps. Because that PID was persistently recorded, the following kill.sh can terminate the current song:
Functionalities such as next, previous, first, last, and set song are implemented by killing the current song, adjusting the song number, and rerunning the script to the left. I made such an animal, which will be discussed later in this TPM issue. |
#!/bin/bash
function on_usr1 ()
{
echo "Song ended normally."
increment_songno.sh
persist "spawnpid=999999" $HOME/littmidi_b.state
kill -s SIGHUP $sleeppid
}
function on_usr2 ()
{
echo "Process was killed!"
persist "spawnpid=999999" $HOME/littmidi_b.state
kill -s SIGHUP $sleeppid
exit 1
}
trap on_usr1 SIGUSR1
trap on_usr2 SIGUSR2
while true; do
songno=$(persist "songno=?" $HOME/littmidi_b.state)
songname=$(songnum2name.sh $songno)
echo songname=$songname
command="timidity $songname > /dev/null"
echo Command=$command
spawn.sh "$command" &
spawnpid=$!
persist "spawnpid=$spawnpid" $HOME/littmidi_b.state
### Next line sleeps forever, relies on a signal
### to kill it.
sleep 1000000 &
sleeppid=$!
wait $sleepid
done
|
As explained
before, trap USR1 to react to a finished song, USR2 to a killed
process, presumably caused by a user request. Create a forever loop that spawns timidity with the proper song using the spawn.sh discussed in the The Cool Thing About This Program article. Once spawn.sh has run, the PID for spawn.sh is saved, and then it goes into a forever sleep. If you look at the code, the USR1 handler increments the song number and kills the sleep, allowing the loop to repeat with the incremented song number. The USR2 handler does the same thing but without the incrementation -- it's assumed that the user will have done whatever is necessary to set the song number. |
#!/bin/bash play.sh > /dev/null & ### WITHOUT THE FOLLOWING SLEEP, ### SUCCESSIVE NEXTS WILL SPAWN ### MULTITUDES OF PLAY.SH ### AND SCREW UP EVERYTHING sleep 0.1 |
playinbackground.sh | This runs play.sh in the background so it doesn't displace UMENU from the terminal. |
#!/bin/bash songno=$(persist "songno=?" $HOME/littmidi_b.state) numsongs=$(persist "numsongs=?" $HOME/littmidi_b.state) echo Currsongno=$songno of $numsongs if test $songno -lt $numsongs; then let songno=$songno+1 else let songno=1 fi persist "songno=$songno" $HOME/littmidi_b.state |
increment_songno.sh | This takes the song
number out
of persistent storage, increments it, and puts it back in persistent
storage. It handles wraparound. Because all my shellscripts work from persistent storage, merely changing the value in persistent storage is sufficient. |
#!/bin/bash songno=$(persist "songno=?" $HOME/littmidi_b.state) numsongs=$(persist "numsongs=?" $HOME/littmidi_b.state) echo Currsongno=$songno of $numsongs if test $songno -gt 1; then let songno=$songno-1 else let songno=$numsongs fi persist "songno=$songno" $HOME/littmidi_b.state |
decrement_songno.sh | Just like increment_songno.sh, except it decrements. It handles wraparound. |
let songno=$1 let oldsongno=$(persist "songno=?" $HOME/littmidi_b.state) let numsongs=$(persist "numsongs=?" $HOME/littmidi_b.state) echo Currsongno=$songno of $numsongs if test $songno -gt $numsongs; then let songno=1 elif test $songno -lt 1; then let songno=$numsongs fi persist "songno=$songno" $HOME/littmidi_b.state |
set_songno.sh | Just like increment_songno.sh, excepts it sets the song number to Arg1. It does quite a bit of testing Arg1 for a valid number between 1 and the length of the playlist, so that a valid new number is stored. |
#!/bin/bash FN=$(persist "playlist=?" $HOME/littmidi_b.state) cat $FN | grep "\.mid\s*$" | grep -v "^\s*$" | grep -v "^\s*#" |
list_playlist.sh | This is how you derive a playlist from the persistent playlist filename and the file to which it refers. The grep commands simply filter out blank lines, comments, and any files not ending in .mid. |
#!/bin/bash SONGNO=$1 list_playlist.sh | head -n $SONGNO | tail -n 1 |
songnum2name.sh | Uses head and tail on the output of list_playlist.sh to deliver the filename corresponding to the song number, for this playlist. |
#!/bin/bash grep "$1=" | sed -e 's/.*=\s*//' | sed -e 's/\s*$//' |
get_value.sh | Given key=value, this delivers the value based on the key. |
#!/bin/bash spawnpid=$(persist "spawnpid=?" $HOME/littmidi_b.state) kill -s SIGHUP $spawnpid persist "spawnpid=999999" $HOME/littmidi_b.state |
killsong.sh | Retrieves
the PID of spawn.sh
from persistent store, and HUPs it. Remember, upon getting HUPped, spawn.sh sends
USR2 to its caller then exits. It also sets the persistent spawnpid as
999999 (in other words, no spawner process running) NOTE: This shellscript was later fitted with code to check whether the process was running before attempting the kill. |
#!/bin/bash PLAYLIST=$1 persist "playlist=$PLAYLIST" $HOME/littmidi_b.state NUMSONGS=$(list_playlist.sh | wc -l) echo dia $NUMSONGS songs in playlist $PLAYLIST. persist "numsongs=$NUMSONGS" $HOME/littmidi_b.state persist "songno=1" $HOME/littmidi_b.state persist "spawnpid=999999" $HOME/littmidi_b.state |
change_playlist.sh | Arg1 is the filename of the new playlist filename. Stores that, along with the number of songs on the playlist. Stores song number as 1 (start at the beginning). |
#!/bin/bash ps ax | grep 'play.sh' | grep -v grep | cut -b1-5 | xargs kill |
nuke_play.sh | When something goes wrong and processes multiply like rabbits, this is how you take them all down at once. |
#!/bin/bash ps ax | grep 'play.sh' | grep -v grep | cut -b1-5 | xargs kill killall timidity ps ax | grep 'sleep 1000000' | grep -v grep | cut -b1-5 | xargs kill |
nuke_all.sh | This is the ultimate weapon of mass destruction. When you run this puppy, all processes associated with the midi player are killed. |
ZZB:::Litt's Text Midi Frontender (bash version) Run Litt's text midi player param D: /d/at/bash/littmidi C: TMPFILE=`mktemp`; C: echo "dir=/scratch/oldsongs/mid/lists/" > $TMPFILE; C: /d/at/cpp/filepicker/fpick $TMPFILE; C: if grep -q "action=select" $TMPFILE; then ACTION=SELECT;fi; C: if test "$ACTION" = "SELECT"; then C: FN=`grep "^file=" $TMPFILE | sed -e "s/.*=//"`; C: persist "playlist=$FN" $HOME/littmidi_b.state; C: persist "songno=1" $HOME/littmidi_b.state; C: numsongs=$(list_playlist.sh | wc -l); C: persist "numsongs=$numsongs" $HOME/littmidi_b.state; C: killsong.sh; C: playinbackground.sh > /dev/null; C: fi; C: rm -f $TMPFILE; pAuse param D: /d/at/bash/littmidi C: killsong.sh; Unpause param D: /d/at/bash/littmidi C: killsong.sh; C: playinbackground.sh; Beginning of song param D: /d/at/bash/littmidi C: killsong.sh; C: playinbackground.sh; First song param D: /d/at/bash/littmidi C: killsong.sh; C: set_songno.sh 1; C: playinbackground.sh; Next param D: /d/at/bash/littmidi C: killsong.sh; C: increment_songno.sh; C: playinbackground.sh; Previous param D: /d/at/bash/littmidi C: killsong.sh; C: decrement_songno.sh; C: playinbackground.sh; Last song param D: /d/at/bash/littmidi C: killsong.sh; C: numsongs=$(persist "numsongs=?" $HOME/littmidi_b.state); C: set_songno.sh $numsongs; C: playinbackground.sh; Jump to song param D: /d/at/bash/littmidi C: echo dia1; C: FN=$(persist playlist=? $HOME/littmidi_b.state); C: TMPFILE=`mktemp`; C: echo dia2; C: persist action= $HOME/littmidi_b.state; C: persist songno= $HOME/littmidi_b.state; C: echo dia3; C: echo "superselect=n" >> $TMPFILE; C: echo "startofdata" >> $TMPFILE; C: echo dia4; C: echo dia4.5 FN=$FN; C: list_playlist.sh >> $TMPFILE; C: echo dia5; C: echo dia6; C: /d/at/cpp/filepicker/rpick $TMPFILE; C: echo dia7; C: cat $TMPFILE; C: ACTION=$(grep "action=" $TMPFILE | get_value.sh "action"); C: if test "$ACTION" = "select"; then C: RECNO=$(grep "recno=" $TMPFILE | get_value.sh "recno"); C: echo dia recnob4=$RECNO; C: let RECNO=$RECNO+1; C: echo dia recno=$RECNO; C: persist "songno=$RECNO" $HOME/littmidi_b.state; C: killsong.sh; C: playinbackground.sh; C: fi; C: rm -f $TMPFILE; S: 1 Change Playlist param D: /scratch/oldsongs/mid/lists C: TMPFILE=`mktemp`; C: echo "dir=/scratch/oldsongs/mid/lists/" > $TMPFILE; C: /d/at/cpp/filepicker/fpick $TMPFILE; C: if grep -q "action=select" $TMPFILE; then ACTION=SELECT;fi; C: if test "$ACTION" = "SELECT"; then C: FN=`grep "^file=" $TMPFILE | sed -e "s/.*=//"`; C: persist "playlist=$FN" $HOME/littmidi_b.state; C: persist "songno=1" $HOME/littmidi_b.state; C: numsongs=$(list_playlist.sh | wc -l); C: persist "numsongs=$numsongs" $HOME/littmidi_b.state; C: cd /d/at/bash/littmidi; C: killsong.sh; C: playinbackground.sh; C: fi; Kill Litts Midi Bash Player param D: /d/at/bash/littmidi C: killsong.sh Zap zombie midi players param C: zapmidizombies S: 1 Volume ::: Mplayer volume Louder param C: VOLM=$(persist "volume=?" $HOME/littmidi_b.state); C: if test "$VOLM" = ""; then C: let VOLM=60; C: persist "volume=$VOLM" $HOME/littmidi_b.state; C: fi; C: if test $VOLM -lt 100; then C: let VOLM=$VOLM+1; C: persist "volume=$VOLM" $HOME/littmidi_b.state; C: aumix -v $VOLM -w $VOLM; C: fi; C: echo "Volume is $VOLM"; Softer param C: VOLM=$(persist "volume=?" $HOME/littmidi_b.state); C: if test "$VOLM" = ""; then C: let VOLM=60; C: persist "volume=$VOLM" $HOME/littmidi_b.state; C: fi; C: if test $VOLM -gt 0; then C: let VOLM=$VOLM-1; C: persist "volume=$VOLM" $HOME/littmidi_b.state; C: aumix -v $VOLM -w $VOLM; C: fi; C: echo "Volume is $VOLM"; Mute param C: aumix -v 0 -w 0; Unmute param C: VOLM=$(persist "volume=?" $HOME/littmidi_b.state); C: if test "$VOLM" = ""; then C: let VOLM=60; C: persist "volume=$VOLM" $HOME/littmidi_b.state; C: fi; C: aumix -v $VOLM -w $VOLM; C: echo "Volume is $VOLM"; Observe Volume param C: VOLM=$(persist "volume=?" $HOME/littmidi_b.state); C: echo Littmidi volume=$VOLM; C: aumix -q; S: 1 seT volume to specific number (0-100) param C: ORG=$(persist "volume=?" $HOME/littmidi_b.state); C: VOLM=%1%Volume please (0-100)%%; C: echo "Original volume=$ORG"; C: echo "$VOLM" | grep "[^[:digit:]]"; C: NOTANUMBER=$?; C: test -z $VOLM; C: ZEROLENGTH=$?; C: if test "$NOTANUMBER" = "1" -a "$ZEROLENGTH" = "1"; then C: persist "volume=$VOLM" $HOME/littmidi_b.state; C: aumix -v $VOLM -w $VOLM; C: echo "New volume is $VOLM"; C: else C: echo "Bad input: $VOLM"; C: echo "Volume unchanged at $ORG"; C: fi; S: 1 ^Quit Special functions ::: Special Functions Menu Persist display param C: cat $HOME/littmidi_b.state S: 1 show pAth: param C: echo Path=$PATH S: 1 pS ax param C: ps ax | less; What time is it? param C: date +%Y/%m/%d__%H:%M:%S S: 1 Edit Litt's Text Music Player menu param C: gvim /d/at/bash/littmidi/zzb.emdl Rebuild Litt's Text Music Player menu param C: /d/bats/rebuild_umenu_zzb.sh Timidity killall param C: killall -s SIGHUP timidity ^Quit plaYlist ::: Playlist menu Get Current Playlist param C: FN=$(persist playlist=? $HOME/littmidi_b.state); C: echo $FN S: 1 Edit Playlist param C: gvim $(persist playlist=? $HOME/littmidi_b.state); ^Quit ^eXit |
"Run" first runs a filepicker so the
user can choose a playlist file.
If the user chooses to select and not
cancel, it persistently stores the
playlist filename, the song number
(strongarmed to 1), and the number
of songs in the playlist.
Finally it kills any current song,
and runs play.sh in the background.
Note that this is essentially just
like the change directory
functionality.
This is much too big to put in an
EMDL file. It really should have
been a script.
=========================
Pause simply kills the current song,
so nothing's playing.
==========================
Unpause re-kills the song just in
case, and then runs play.sh in the
background.
===========================
Beginning kills the current song
and runs play.sh in the background
to replay the current song from the
beginning.
==========================
First song sets the song number to 1,
kills the current song and plays the
(now) current song.
==========================
Next kills the current song,
increments the persistent song
number, and plays the (now) current
song.
===========================
Previous is just like Next except it
decrements.
===========================
Last song sets the song number to the
number of songs in the current
playlist, then kills and replays.
===========================
Jump to song uses list_playlist.sh
to create a recordpicker from which
the user chooses a song. The
recordpicker delivers a 0 based
record number, which is incremented
to get a 1 based song number, which
is stored persistently, after which
there's a kill and play.
This is much too big to put in an
EMDL file. It really should have
been a script.
==========================
Change Playlist runs a filepicker,
stores the user's choice as the
playlist filename, derives the number
of songs and stores that, stores the
song number as 1, and stores the
spawner PID as 999999, meaning no
spawner.
This is much too big to put in an
EMDL file. It really should have
been a script.
==================================
Kills the current player and does
not replace it.
==================================
This Zap is for the old Ruby based
player, and needs recoding for the
bash based player.
==================================
The volume functions store a single
volume number, and set aumix's
master volume AND pcm to that value.
================================
The special functions are mostly
diagnostic or compilation.
================================
Persist Display displays all
variables in peristent storage.
================================
Show path shows the current path.
================================
ps ax shows that command in a less
pager.
================================
What time is it shows the time.
================================
Edit Litt's Text Music Player lets
you edit the EMDL file.
================================
Rebuild compiles the EMDL file into
a menu, shows any errors or warnings,
and gives you the choice of deploying
(transferring) the new menu or not.
================================
Timidity Killall kills all Timidity
processes.
================================
|
#!/bin/bash TMPFILE=mktemp cd /d/at/bash/littmidi persist "action=cancel" $HOME/littmidi_b.state echo "dir=/scratch/oldsongs/mid/lists/" > $TMPFILE /d/at/cpp/filepicker/fpick $TMPFILE if grep -q "action=select" $TMPFILE; then ACTION=SELECT fi; if test "$ACTION" = "SELECT"; then FN=`grep "^file=" $TMPFILE | sed -e "s/.*=//"` change_playlist.sh $FN persist "action=select" $HOME/littmidi_b.state fi rm -f $TMPFILE |
change_playlist_ui.sh | This runs the
filepicker, stores the action, and if the action was select it runs change_playlist.sh
to store everything else that should be stored. It's up to the UMENU part of the process to actually kill and rerun. |
#!/bin/bash TMPFILE=`mktemp` echo "superselect=n" >> $TMPFILE echo "startofdata" >> $TMPFILE list_playlist.sh >> $TMPFILE /d/at/cpp/filepicker/rpick $TMPFILE ACTION=$(grep "action=" $TMPFILE | get_value.sh "action") persist "action=$ACTION" $HOME/littmidi_b.state if test "$ACTION" = "select"; then RECNO=$(grep "recno=" $TMPFILE | get_value.sh "recno") let RECNO=$RECNO+1 set_songno.sh $RECNO fi rm -f $TMPFILE |
jump2song.sh | This runs the
recordpicker against the playlist's songs, accepts the user's choice,
stores the action (select
or cancel), and if select it runs set_songno.sh
to store relevent info. It's up to the UMENU part of the process to actually kill and rerun. |
Change Playlist param C: change_playlist_ui.sh; C: action=$(persist "action=?" $HOME/littmidi_b.state); C: if test "$action" = "select"; then C: killsong.sh; C: playinbackground.sh; C: fi; S: 1 |
Change Playlist | Here change_playlist_ui does almost all the work, including querying the user with a filepicker. All UMENU does is make sure the user selected rather than cancelled, and if so kill and rerun. |
Jump to song param C: jump2song.sh; C: action=$(persist action=? $HOME/littmidi_b.state); C: if test "$action" = "select"; then C: killsong.sh; C: playinbackground.sh; C: fi S: 1 |
Jump to song | Here jump2song.sh does most of the work, including querying the user with a recordpicker. All UMENU does is test that the user selected instead of cancelled, and if so kills and replays. |
#!/bin/bash while true; do cat /d/bats/littmidi.fifo done |
#!/bin/bash TMPFILE=`mktemp` echo "superselect=n" >> $TMPFILE echo "startofdata" >> $TMPFILE list_playlist.sh >> $TMPFILE /d/at/cpp/filepicker/rpick $TMPFILE ACTION=$(grep "action=" $TMPFILE | get_value.sh "action") persist "action=$ACTION" $HOME/littmidi_b.state exitcode=1 if test "$ACTION" = "select"; then RECNO=$(grep "recno=" $TMPFILE | get_value.sh "recno") let RECNO=$RECNO+1 set_songno.sh $RECNO exitcode=0 fi rm -f $TMPFILE exit $exitcode |
The
code to the right
picks a song and delivers the song name to persistent storage. There's
no way around that. But it also delivers the user's action (select or
cancel) to persistent storage. That's unnecessary because that
information can be delivered back via the exit code (0 for select, 1
for cancel). This way the program that called the code to the left changes the song if this code's exit value is 0, but does nothing if it's nonzero. If the calling program had looked at the "action" variable in persistent disk storage, and if some other process had subsequently modified that value (difficult but not impossible), the calling program would do the wrong thing and display a hard to find bug. Exit codes are by their nature modular because only the process' caller can see them. |
| Foreground
Child (songnum2name.sh) |
Parent | |||
#!/bin/bash SONGNO=$1 list_playlist.sh | head -n $SONGNO | tail -n 1 |
songno=$(persist "songno=?" $HOME/littmidi_b.state) songname=$(songnum2name.sh $songno) |
The parent gets the song number, then runs songnum2name.sh and sets $songname to the output of songnum2name. sh.songnum2name.sh lists all songs, and uses head and tail to grab the songname corresponding to $1. |
| MODULARITY TIPS | ||
FAVOR
THESE
|
LIMIT
THESE
|
|
|
AUTHOR'S NOTE
The output of many of the ps ax commands in this article were altered to make the output less wide. In many cases I did things like substitute "gimp", "dia", "timidity ./test.mid", or "gvim test.sh" for much longer lines in the output. I also reduced the number of spaces between the words "hangup" and "sleep". That being said, in no case did I alter any line containing ./test.sh or sleep, so for the purposes of this article, the ps commands are proof of the existence of nonexistence of the sleep or test.sh processes. |
| Representation | What it Represents | |
$$ |
PID of current process | |
$! |
PID of last background process that was run | |
$PPID |
PID of parent of current process | |
$? |
Exit status of last foreground process run |
grep "George Bush" myfile.txt # See whether file contains president's name georgethere=$? # Store grep's return code in $georgethere timidity ./test.mid & # Run timidity in the background timidity_pid=$! # Store timidity's PID in timidity_pidOnce so stored, you can use these values anywhere, including in signal handler routines, even after many other processes have been run, both foreground and background. If the return code of a foreground process is important, always save $? immediately upon termination of the foreground process. If the PID of a process your script has run in the background is important, always save $! immediately after running it in the background.
timidity ./test.mid & # Run timidity in the background timidity_pid=$! # Store timidity's PID in timidity_pid echo $timidity_pid > tim.pid # Store it to diskNow, if any process wants to send, let's say, a USR1 signal to that timidity process, it's as simple as this:
kill -s SIGUSR1 $(cat tim.pid)
|
NOTE
As mentioned in the Build AAuSE Modularly article, storing data on disk
decreases modularity, so do it only when necessary. Note also that if
you store it to a filename known only to the process receiving the
information (perhaps the filename contains the receiver's PID), and if
the receiving process deletes it immediately after storing it to a
variable, this increases modularity over using a descriptive filename. |
timidity ./test.mid & # Run timidity in the background
timidity_pid=$! # Store timidity's PID in timidity_pid
# do other stuff
ls -ldF /proc/$timidity_pid > /dev/null 2>&1 # Check if the PID has a directory in /proc
$stillrunning=$? # If it's still running, $stillrunning is 0
# otherwise $stillrunning will be nonzero
#!/bin/bash mypid=$$ mycommand="kill -s SIGUSR1 $mypid" echo $mycommand > $HOME/danger.sh chmod a+x $HOME/danger.sh sleep 30 echo FELL THROUGH |
[slitt@mydesk ~]$ ./test.sh User defined signal 1 [slitt@mydesk ~]$ ./test.sh FELL THROUGH [slitt@mydesk ~]$ |
First, notice that
the script writes a second script, danger.sh,
which kills the job number corresponding to the running script.
Recording the PID of the running script so others can signal it is a
very common technique. Running danger.sh
sends a USR1 signal to the running script -- a fast way to send the
signal. The first time ./danger.sh was run in order to kill the script. The script terminated immediately and did not print the final message. On the second run, the script was not called, so the script ran its 30 seconds and printed the final message. |
#!/bin/bash function donothing() { echo -n } trap donothing SIGUSR1 mypid=$$ mycommand="kill -s SIGUSR1 $mypid" echo $mycommand > $HOME/danger.sh chmod a+x $HOME/danger.sh sleep 30 echo FELL THROUGH |
[slitt@mydesk ~]$ ./test.sh FELL THROUGH [slitt@mydesk ~]$ |
Here, whether ./danger.sh
was run or not, the script ran all the way to the end, SIGUSR1 signal
executed
function donothing() instead of killing the script. I've seen documentation stating you could disable a signal like this: trap SIGUSR1I can tell you that on my system, that doesn't work, and the script will be terminated by receipt of a SIGUSR1. On my system, you must include either the function name or an empty string: trap "" SIGUSR1The preceding trap completely ignores SIGUSR1, to the extent that a current wait is not breached. |
#!/bin/bash function dosomething() { echo "Hit me with your best shot!" } trap dosomething SIGUSR1 mypid=$$ mycommand="kill -s SIGUSR1 $mypid" echo $mycommand > $HOME/danger.sh chmod a+x $HOME/danger.sh sleep 30 echo FELL THROUGH |
[slitt@mydesk ~]$ ./test.sh Hit me with your best shot! FELL THROUGH [slitt@mydesk ~]$ ./test.sh FELL THROUGH [slitt@mydesk ~]$ |
Here I ran danger.sh six
times, but the message printed only once, and not until the sleep
was over. That's not a good example of "doing useful work". What we
want is that every time we hit it with a USR1, it prints the "hit me"
message. Could it have to do with the fact that sleep was in the foreground when the interrupts hit? |
#!/bin/bash
function dosomething()
{
echo "Hit me with your best shot!"
}
trap dosomething SIGUSR1
mypid=$$
mycommand="kill -s SIGUSR1 $mypid"
echo $mycommand > $HOME/danger.sh
chmod a+x $HOME/danger.sh
sleep 30 &
sleeppid=$!
wait $sleeppid
echo FELL THROUGH
|
[slitt@mydesk ~]$ ./test.sh Hit me with your best shot! FELL THROUGH [slitt@mydesk ~]$ |
Yep -- now the
signal is handled instantly. But there's trouble. The instant you issue the ./danger.sh command, the script terminates but performs the signal handler routine and prints the last statement. A ps command shows that the sleep command is still running, so the problem isn't that the SIGUSR1 got passed on to the sleep command -- the problem is that either the interrupt or the starting of the handler terminated the wait command. Remember, what we want is for the "hit me" message to print immediately after each USR1 signal. |
#!/bin/bash
function dosomething()
{
echo "Hit me with your best shot!"
wait $sleeppid
}
trap dosomething SIGUSR1
mypid=$$
mycommand="kill -s SIGUSR1 $mypid"
echo $mycommand > $HOME/danger.sh
chmod a+x $HOME/danger.sh
sleep 30 &
sleeppid=$!
wait $sleeppid
echo FELL THROUGH
|
[slitt@mydesk ~]$ ./test.sh Hit me with your best shot! Hit me with your best shot! FELL THROUGH [slitt@mydesk ~]$ |
I issued the ./danger.sh
command five times. The first time the "hit me" message printed
instantly. Then the script did nothing more until the sleep
terminated, after which it printed one more "hit me" message and then
fell through and printed the final message. Ugh!!! |
#!/bin/bash
function dosomething()
{
echo "Hit me with your best shot!"
}
trap dosomething SIGUSR1
mypid=$$
mycommand="kill -s SIGUSR1 $mypid"
echo $mycommand > $HOME/danger.sh
chmod a+x $HOME/danger.sh
sleep 30 &
sleeppid=$!
loopvar=999999
while test $loopvar -ne 0; do
wait $sleeppid
loopvar=$?
done
echo FELL THROUGH
|
[slitt@mydesk ~]$ ./test.sh FELL THROUGH [slitt@mydesk ~]$ ./test.sh Hit me with your best shot! Hit me with your best shot! Hit me with your best shot! Hit me with your best shot! Hit me with your best shot! FELL THROUGH [slitt@mydesk ~]$ |
Ah, now that's more
like it. This version prints the "hit me" message immediately upon
running ./danger.sh,
and keeps printing it immediately every time. When the waited for sleep
command finally terminates, the loop ends and we fall through. It turns out that the signal interrupts the wait command, which returns 138 when so interrupted. It returns 0 when the waited-for process times out. So we just loop through waits until one returns 0. Notice this is NOT polling. The loop is iterated only when a SIGUSR1 is received. Otherwise it sits and waits. |
#!/bin/bash
function dosomething()
{
echo "Hit me with your best shot!"
}
function terminate_loop()
{
loopvar=0
}
trap dosomething SIGUSR1
trap terminate_loop SIGUSR2
mypid=$$
mycommand="kill -s SIGUSR1 $mypid"
echo $mycommand > $HOME/danger.sh
chmod a+x $HOME/danger.sh
mycommand="kill -s SIGUSR2 $mypid"
echo $mycommand > $HOME/danger2.sh
chmod a+x $HOME/danger2.sh
sleep 1000000 &
sleeppid=$!
loopvar=999999
while test $loopvar -ne 0; do
wait $sleeppid
done
kill -s SIGHUP $sleeppid
echo FELL THROUGH
ps ax | tail -n 8
|
[slitt@mydesk ~]$ ./test.sh Hit me with your best shot! Hit me with your best shot! FELL THROUGH 7612 pts/10 S+ 0:00 /usr/bin/less -isr 7718 ? S 0:00 kcalc 7766 ? S 0:00 man bash 7986 pts/4 S+ 0:00 gimp 7987 pts/4 S+ 0:01 dia 7996 pts/6 S+ 0:00 /bin/bash ./test.sh 8006 pts/6 R+ 0:00 ps ax 8007 pts/6 R+ 0:00 tail -n 8 ./test.sh: line 32: 7999 Hangup sleep 1000000 [slitt@mydesk ~]$ |
In the preceding, we ran danger.sh twice to hit it with USR1, and then ran danger2.sh to hit it with USR2, which terminated the loop. The final ps shows that the sleep is no longer running, due to the kill just before the script ends. |
#!/bin/bash
function dosomething()
{
echo "Hit me with your best shot!"
}
function terminate_loop()
{
kill -s SIGHUP $sleeppid
loopvar=0
}
trap dosomething SIGUSR1
trap terminate_loop SIGUSR2
mypid=$$
mycommand="kill -s SIGUSR1 $mypid"
echo $mycommand > $HOME/danger.sh
chmod a+x $HOME/danger.sh
mycommand="kill -s SIGUSR2 $mypid"
echo $mycommand > $HOME/danger2.sh
chmod a+x $HOME/danger2.sh
sleep 1000000 &
sleeppid=$!
loopvar=999999
while test $loopvar -ne 0; do
wait $sleeppid
done
echo FELL THROUGH
ps ax | tail -n 8
|
[slitt@mydesk ~]$ ./test.sh Hit me with your best shot! Hit me with your best shot! FELL THROUGH 7602 pts/10 S+ 0:00 man bash 7605 pts/10 S+ 0:00 gimp 7606 pts/10 S+ 0:00 dia 7612 pts/10 S+ 0:00 /usr/bin/less -isr 7718 ? S 0:00 kcalc 7766 ? S 0:00 timidity ./test2.mid 8504 pts/6 S+ 0:00 /bin/bash ./test.sh 8519 pts/6 R+ 0:00 ps ax ./test.sh: line 32: 8507 Hangup sleep 1000000 [slitt@mydesk ~]$ |
This works the same
way, but you know that no matter what kind of break statements are used
in the loop, the sleep
command will terminate. Of course, if an untrapped signal terminates the script, sleep will live on forever (or for 11 days). |
#!/bin/bash
function dosomething()
{
echo "USR1 encountered"
}
function terminate_loop()
{
echo "USR2 encountered"
kill -s SIGHUP $sleeppid
loopvar=0
}
function exit_cleanly()
{
echo "Misc signal encountered"
kill -s SIGHUP $sleeppid
exit 1
}
trap exit_cleanly SIGHUP
trap exit_cleanly SIGINT
trap exit_cleanly SIGQUIT
trap exit_cleanly SIGILL
trap exit_cleanly SIGTRAP
trap exit_cleanly SIGABRT
trap exit_cleanly SIGBUS
trap exit_cleanly SIGFPE
trap exit_cleanly SIGKILL
trap exit_cleanly SIGUSR1
trap exit_cleanly SIGSEGV
trap exit_cleanly SIGUSR2
trap exit_cleanly SIGPIPE
trap exit_cleanly SIGALRM
trap exit_cleanly SIGTERM
trap exit_cleanly SIGSTKFLT
trap exit_cleanly SIGCHLD
trap exit_cleanly SIGCONT
trap exit_cleanly SIGSTOP
trap exit_cleanly SIGTSTP
trap exit_cleanly SIGTTIN
trap exit_cleanly SIGTTOU
trap exit_cleanly SIGURG
trap exit_cleanly SIGXCPU
trap exit_cleanly SIGXFSZ
trap exit_cleanly SIGVTALRM
trap exit_cleanly SIGPROF
trap exit_cleanly SIGWINCH
trap exit_cleanly SIGIO
trap exit_cleanly SIGPWR
trap exit_cleanly SIGSYS
trap dosomething SIGUSR1
trap terminate_loop SIGUSR2
mypid=$$
mycommand="kill -s SIGUSR1 $mypid"
echo $mycommand > $HOME/danger.sh
chmod a+x $HOME/danger.sh
mycommand="kill -s SIGUSR2 $mypid"
echo $mycommand > $HOME/danger2.sh
chmod a+x $HOME/danger2.sh
sleep 1000000 &
sleeppid=$!
loopvar=999999
while test $loopvar -ne 0; do
wait $sleeppid
done
echo FELL THROUGH
ps ax | tail -n 8
|
[slitt@mydesk ~]$ ./test.sh USR1 encountered USR1 encountered USR2 encountered FELL THROUGH 7612 pts/10 S+ 0:00 /usr/bin/less -isr 7718 ? S 0:00 kcalc 7766 ? S 0:00 timidity ./test.sh 8534 pts/3 Sl 0:01 gimp 8537 pts/3 S 0:00 gvim test.sh 8541 pts/3 S 0:01 dia 8661 pts/6 S+ 0:00 /bin/bash ./test.sh 8669 pts/6 R+ 0:00 ps ax [slitt@mydesk ~]$ ./test.sh Misc signal encountered [slitt@mydesk ~]$ |
After running the
script, from
another terminal I hit it with two USR1 signals and one USR2, which
killed the sleep and caused the loop to terminate. I then ran it again and pressed Ctrl+C, which sent a SIGINT to the script. SIGINT was trapped to exit_cleanly(), which killed sleep and then exited immediately. Notice that USR1 and USR2 were trapped twice, and that the later traps overrode the earlier ones. That's important. Notice also that I could have done all the miscellaneous trapping in a single statement, listing all the interrupts after the handler routine. That's perfectly legal, but it makes for a very long line, and also makes it more difficult when you want to use binary search to find out what signal is hitting you. More on that later. |