#!/bin/sh
# -*- tcl -*-
# The next line is executed by /bin/sh, but not tcl \
exec tclsh "$0" ${1+"$@"}

package require Expect


# dislocate - allow disconnection and reconnection to a background program
# Author: Don Libes, NIST

exp_version -exit 5.1

# The following code attempts to intuit whether cat buffers by default.
# The -u flag is required on HPUX (8 and 9) and IBM AIX (3.2) systems.
if {[file exists $exp_exec_library/cat-buffers]} {
    set catflags "-u"
} else {
    set catflags ""
}
# If this fails, you can also force it by commenting in one of the following.
# Or, you can use the -catu flag to the script.
#set catflags ""
#set catflags "-u"

set escape \035			;# control-right-bracket
set escape_printable "^\]"

set pidfile "~/.dislocate"
set prefix "disc"
set timeout -1
set debug_flag 0

while {$argc} {
    set flag [lindex $argv 0]
    switch -- $flag \
	    "-catu" {
	set catflags "-u"
	set argv [lrange $argv 1 end]
	incr argc -1
    } "-escape" {
	set escape [lindex $argv 1]
	set escape_printable $escape
	set argv [lrange $argv 2 end]
	incr argc -2
    } "-debug" {
	log_file [lindex $argv 1]
	set debug_flag 1
	set argv [lrange $argv 2 end]
	incr argc -2
    } default {
	break
    }
}

# These are correct from parent's point of view.
# In child, we will reset these so that they appear backwards
# thus allowing following two routines to be used by both parent and child
set  infifosuffix ".i"
set outfifosuffix ".o"

proc infifoname {pid} {
    return "/tmp/$::prefix$pid$::infifosuffix"
}

proc outfifoname {pid} {
    return "/tmp/$::prefix$pid$::outfifosuffix"
}

proc pid_remove {pid} {
    say "removing $pid $::proc($pid)"

    unset ::date($pid)
    unset ::proc($pid)
}

# lines in data file look like this:
# pid#date-started#argv

# allow element lookups on empty arrays
set date(dummy) dummy;	unset date(dummy)
set proc(dummy) dummy;	unset proc(dummy)

proc say {msg} {
    if {!$::debug_flag} return

    if {[catch {puts "parent: $msg"}]} {
	send_log "child: $msg\n"
    }
}

# load pidfile into memory
proc pidfile_read {} {
    global date proc pidfile

    say "opening $pidfile"
    if {[catch {open $pidfile} fp]} return

    #
    # read info from file
    #

    say "reading pidfile"
    set line 0
    while {[gets $fp buf]!=-1} {
	# while pid and date can't have # in it, proc can
	if {[regexp "(\[^#]*)#(\[^#]*)#(.*)" $buf junk pid xdate xproc]} {
	    set date($pid) $xdate
	    set proc($pid) $xproc
	} else {
	    puts "warning: inconsistency in $pidfile line $line"
	}
	incr line
    }
    close $fp
    say "read $line entries"

    #
    # see if pids and fifos are still around
    #

    foreach pid [array names date] {
	if {$pid && [catch {exec /bin/kill -0 $pid}]} {
	    say "$pid no longer exists, removing"
	    pid_remove $pid
	    continue
	}

	# pid still there, see if fifos are
	if {![file exists [infifoname $pid]] || ![file exists [outfifoname $pid]]} {
	    say "$pid fifos no longer exists, removing"
	    pid_remove $pid
	    continue
	}
    }
}

proc pidfile_write {} {
    global pidfile date proc

    say "writing pidfile"

    set fp [open $pidfile w]
    foreach pid [array names date] {
	puts $fp "$pid#$date($pid)#$proc($pid)"
	say "wrote $pid#$date($pid)#$proc($pid)"
    }
    close $fp
}

proc fifo_pair_remove {pid} {
    global date proc prefix

    pidfile_read
    pid_remove $pid
    pidfile_write

    file delete -force [infifoname $pid] [outfifoname $pid]
}

proc fifo_pair_create {pid argdate argv} {
    global prefix date proc

    pidfile_read
    set date($pid) $argdate
    set proc($pid) $argv
    pidfile_write

    mkfifo [infifoname $pid]
    mkfifo [outfifoname $pid]
}

proc mkfifo {f} {
    if {[file exists $f]} {
	say "uh, fifo already exists?"
	return
    }

    if {0==[catch {exec mkfifo $f}]} return		;# POSIX
    if {0==[catch {exec mknod $f p}]} return
    # some systems put mknod in wierd places
    if {0==[catch {exec /usr/etc/mknod $f p}]} return	;# Sun
    if {0==[catch {exec /etc/mknod $f p}]} return	;# AIX, Cray
    puts "Couldn't figure out how to make a fifo - where is mknod?"
    exit
}

proc child {argdate argv} {
    global infifosuffix outfifosuffix

    disconnect
    # these are backwards from the child's point of view so that
    # we can make everything else look "right"
    set  infifosuffix ".o"
    set outfifosuffix ".i"
    set pid 0

    eval spawn $argv
    set proc_spawn_id $spawn_id

    while {1} {
	say "opening [infifoname $pid] for read"
	
	set catfid [open "|cat $::catflags < [infifoname $pid]" "r"]
	set ::catpid $catfid
	spawn -open $catfid
	set in $spawn_id

	say "opening [outfifoname $pid] for write"
	spawn -open [open [outfifoname $pid] w]
	set out $spawn_id

	fifo_pair_remove $pid

	say "interacting"
	interact {
	    -u $proc_spawn_id eof exit
	    -output $out
	    -input $in
	}

	# parent has closed connection
	say "parent closed connection"
	catch {close -i $in}
	catch {wait -i $in}
	catch {close -i $out}
	catch {wait -i $out}

	# switch to using real pid
	set pid [pid]
	# put entry back
	fifo_pair_create $pid $argdate $argv
    }
}

proc escape {} {
    # export process handles so that user can get at them
    global in out

    puts "\nto disconnect, enter: exit (or ^D)"
    puts "to suspend, press appropriate job control sequence"
    puts "to return to process, enter: return"
    interpreter -eof exit
    puts "returning ..."
}

# interactively query user to choose process, return pid
proc choose {} {
    while {1} {
	send_user "enter # or pid: "
	expect_user -re "(.*)\n" {set buf $expect_out(1,string)}
	if {[info exists ::index($buf)]} {
	    set pid $::index($buf)
	} elseif {[info exists ::date($buf)]} {
	    set pid $buf
	} else {
	    puts "no such # or pid"
	    continue
	}
	return $pid
    }
}

if {$argc} {
    # initial creation occurs before fork because if we do it after
    # then either the child or the parent may have to spin retrying
    # the fifo open.  Unfortunately, we cannot know the pid ahead of
    # time so use "0".  This will be set to the real pid when the
    # parent does its initial disconnect.  There is no collision
    # problem because the fifos are deleted immediately anyway.

    set datearg [clock format [clock seconds]]

    fifo_pair_create 0 $datearg $argv

    # to debug by faking child, comment out fork and set pid to a
    # non-zero int, then you can read/write to pipes manually

    set pid [fork]
    say "after fork, pid = $pid"
    if {$pid==0} {
	child $datearg $argv
    }

    # parent thinks of child as pid==0 for reason given earlier
    set pid 0
}

say "examining pid"

if {![info exists pid]} {
    global fifos date proc

    say "pid does not exist"

    pidfile_read

    set count 0
    foreach pid [array names date] {
	incr count
    }

    if {$count==0} {
	puts "no connectable processes"
	exit
    } elseif {$count==1} {
	puts "one connectable process: $proc($pid)"
	puts "pid $pid, started $date($pid)"
	send_user "connect? \[y] "
	expect_user -re "(.*)\n" {set buf $expect_out(1,string)}
	if {$buf!="y" && $buf!=""} exit
    } else {
	puts "connectable processes:"
	set count 1
	puts " #   pid      date started      process"
	foreach pid [array names date] {
	    puts [format "%2d %6d  %.19s  %s" \
		    $count $pid $date($pid) $proc($pid)]
	    set index($count) $pid
	    incr count
	}
	set pid [choose]
    }
}

say "opening [outfifoname $pid] for write"
spawn -noecho -open [open [outfifoname $pid] w]
set out $spawn_id

say "opening [infifoname $pid] for read"
set catfid [open "|cat $catflags < [infifoname $pid]" "r"]
set catpid [pid $catfid]
spawn -noecho -open $catfid
set in $spawn_id

puts "Escape sequence is $escape_printable"

proc prompt1 {} {
    return "$::argv0[history nextid]> "
}

rename exit exitReal

proc exit {} {
    exec /bin/kill $::catpid
    exitReal
}

interact {
    -reset $escape escape
    -output $out
    -input $in
}


