#! ../scotty -nf
##
## This script checks if a list of process is still running.
##
## The main purpose of this script is to recognize dying system
## daemons. The mach OS has a meta server called nanny that restarts
## dying system daemons automatically. This script just checks if
## daemons have died and writes a message using the logger(1). This
## is not as nice as nanny but portable and better than nothing.
##
## To use yanny, you must write the pids and the names of the system
## daemons to a file (usually /etc/yanny.conf) during bootup. yanny
## reads this file and test if the daemons listed there are running.
##
## The easiest way to do this is to modify your /etc/rc* files to
## put the pids in a file like /etc/yanny.conf. Below is an example
## for the rpc.statd daemon.
##
## if [ -f /usr/etc/rpc.statd ]; then
##        yanny rpc.statd & echo -n ' statd'
## fi
##
##

##
## Get the list of running processes. This proc must guess which
## ps command to use. Running processes are stored in the global
## array processes indexed by the pid and containing the process 
## name.
##

proc read_ps_ax {} {

    global processes
    catch {unset processes}

    set ps [open "| ps -axc"]
    gets $ps line
    while {![eof $ps]} {
	set pid [lindex $line 0]
	regsub {^[^:]*:[0-9][0-9]} $line "" cmd
	set processes($pid) [string trim $cmd]
	gets $ps line
    }
}

proc read_ps_e {} {

    global processes
    catch {unset processes}

    set ps [open "| ps -e"]
    gets $ps line
    while {![eof $ps]} {
	set pid [lindex $line 0]
        regsub {^[^:]*:[0-9][0-9]} $line "" cmd
        set processes($pid) [string trim $cmd]
        gets $ps line
    }
}

proc read_ps {} {
    if {[catch read_ps_ax]} {
	if {[catch read_ps_e]} {
	    puts stderr "yanny: unable to get process status"
	    exit 1
        }
    }
}

##
## Read the list of pids and daemon names from a file.
## Ignore comment lines starting with #.
##

proc read_daemons {fname} {
    global daemons
    if {![file exists $fname]} {
	return 0
    }
    if {![file readable $fname]} {
	puts stderr "yanny: unable to open $fname"
	exit
    }
    set cnt 0
    set fh [open $fname]
    while {![eof $fh]} {
	gets $fh line
	if {($line == "") || ([string index $line 0] == "#")} continue
	set line [split [string trim $line] ":"]
	set daemons([lindex $line 0]) [lrange $line 1 end]
	incr cnt
    }
    close $fh
    return $cnt
}

##
## Write the pids and daemon names to a file.
##

proc write_daemons {fname} {
    global daemons count unique

    if {[catch {open $fname w} fh]} {
	puts stderr "yanny: unable to open $fname"
	exit
    }

    if {[info exists daemons]} {
	foreach pid [array names daemons] {
	    puts $fh "$pid:[lindex $daemons($pid) 0]:[join [lrange $daemons($pid) 1 end]]"
	}
    }

    close $fh
}

##
## Write the result to the syslog if available.
##

proc write_logger {text} {
    syslog "yanny: $text"
    puts $text
}

##
## Restart a daemon.
##

proc restart {cmd} {
    global daemons
    write_logger "restarting $cmd"
    set pid [eval exec $cmd &]
    set pname [file tail [lindex $cmd 0]]
    set daemons($pid) "$pname $cmd"
}

##
## Check if the pids of the daemons really exist.
##

proc check_daemon {fname} {
    global daemons processes

    if {[read_daemons $fname] == 0} return
    
    read_ps

    foreach pid [array names daemons] {
	set name [lindex $daemons($pid) 0]
	if {![info exists processes($pid)]} {
	    write_logger "$name ($pid) gone away"
	    restart [join [lindex $daemons($pid) 1]]
	    unset daemons($pid)
	    continue
	}
	if {![regexp $name $processes($pid)]} {
	    write_logger "$name ($pid) gone away (pid recycled)"
	    restart [join [lindex $daemons($pid) 1]]
            unset daemons($pid)
	}
    }

    write_daemons $fname
}

##
## Add a daemon to the list of daemons to care about.
## Start it from here and save its pid.
##

proc add_daemon {fname cmd} {
    read_daemons $fname
    restart $cmd
    write_daemons $fname
}

##
## Kill an entry from the config file.
##

proc kill_daemon {fname reg} {
    global daemons
    
    if {[read_daemons $fname] == 0} {
	puts "yanny: file $fname does not exist."
	return
    }
    
    foreach pid [array names daemons] {
	if {[regexp $reg $daemons($pid)]} {
	    puts "removing $pid $daemons($pid)"
	    exec /bin/kill $pid
	    unset daemons($pid)
	}
    }
    
    write_daemons $fname
}

##
## The main program. Check for the -f option and decide if we 
## add a new daemon or if we check the actual listing.
##

set fname /etc/yanny.conf

##
## Parse options to do some magic stuff.
##

set uniq 0
set kill 0

set newargv ""
foreach arg $argv {
    if {[string index $arg 0] == "-"} {
	switch -- $arg  {
	    "-f" { lappend newargv $arg }
	    "-u" { incr argc -1; set uniq 1 }
	    "-k" { incr argc -1; set kill 1 }
	}
    } else {
	lappend newargv $arg
    }
}
set argv $newargv

if {$argc > 1 && [lindex $argv 0] == "-f"} {
    set fname [lindex $argv 1]
    set argv [lrange $argv 2 end]
    incr argc -2
}

if {$argc == 0} {
    check_daemon $fname
} else {
    if {$kill} {
	kill_daemon $fname [join $argv]
    } else {
	add_daemon $fname [join $argv]
    }
}
