#
# <20231214.1537.47>
#
# This is the begining spec. for a Virtual File System for use with Filerunner.
#
# The intent is to provide a frame work for a generalized VFS for inside of
# Filerunner. We are NOT trying to provide access to any other program to the
# virtual files we are working with, however, for those VFS implementations
# that map into (read 'mount') normal file system space, such access is
# available.
#
# When we started, Filerunner already had three vfs' up and running:
#
# ftp://
# sftp:// 
# aftp:// (aftp: being access through adb to such devices as it talks to)
#
# This is an attempt to generalize this and inclued many more types of
# vfs'. In particular, we want to be able to handel:
#
# zip, tar tar+comp such as tar.gz any archive sort of structure
#                   including rpm, deb and msi
#
# It is hoped that we can generalize this enough that a user can easily add
# a new type by supplying only a few key parts.
#
# Here we define the basic structure and use of VFS.
# We define two types:
# Type I  requires code for most (if not all) of the accesses. These are usually
#         accesses to file structure on remote systems. ftp://, sftp:// and such
#         are examples
# Type II requires only setup (open/mount) and close/umount. It is usually
#         completely local (although it may cast a remote system to a different
#         format for access). Examples of this form of VFS are zip, tar, etc.

# Type II is implemented by providing an open/mount script and a close/unmount
#         script. These scripts may call external programs, but once a given
#         Type II VFS is mounted it is treated and behaves as a normal file
#         system, i.e. open/close/read/write etc. work within and out side
#         of filerunner. This means we can use all of our normal scripts
#         to display modify and manage these files.
#         The scrits that filerunner provides for Type II VFS mounts are:
#         1) The choosing and execution of the open/mount and close/umount
#            scripts.
#         2) The DisplayName structure that puts up a different name for the
#            VFS mount point.
#         3) Logic to allow nested Type II VFS mounts and their proper close/umount
#
#         Each Type II VFS subtype (e.g. zip, tar, etc.) requires a structure
#         that tells the 'mount' command that it handles this particular subtype.
#         Usually this is done by passing the 'mount' command the name of the
#         file to mount (e.g. subtype zip might have a *.zip as its selection
#         criteria) the same as the "view" command does. The 'mount' command finds
#         the required list entry which will have a script to run to do the
#         mount.


# The following code implements Type I VFS
# Identifying a Type I VFS.
# A particular VFS is identified by a URL type of reference. For our use we
# are interested in identifing that we have a URL (as apposed to a simple file name)
# Here is a full blown URL:
# <protocol>://<address><path>
# <protocol> is something like "file", "https", "http", "zip", "tar", etc.
# ://        is what we use to suggest that this is a URL rather than a simple file or dir
# <address>  is (possibly) composed of a set of 1 or more names seperated by dots (.)
#            and optionally followed by :<port>. This <port> is usually a number but
#            we do not require this to be so. It may be preceded by "user@" where user
#            is the user on the given host address to login as.
#            The <address> is terminated by the '/'
#            (forward slash) which can also be considered part of the <path>. That is:
# <path>     starts with '/' and has the same format as a normal file.

#
# IsVF <URL>
#
# This command tests for a VFS URL. If it is of proper form we populate the variables:
#   VFStok     with <protocol>://<address>
#   VFSpro     with the <protocol>
#   VFSadd     with the <address>
#   VFSpath    with the <path>
# We have extra stuff here to allow pro:// to be VFS while pro:/// is not.
# This makes it easier to do URL join, below.
# Also, the path is separated from the address by "/" which is NOT part of the
# path or the address. Thus p://ad & p://ad/ both yield path={}, p://ad// yields
# path "/" and p://ad/foo yields relative path 'foo'. Mostly we deal with
# absolute paths thus usuall we will see p://ad//...
# proc IsVFS1 {url} {
#   return [uplevel regexp {^((\[^:\]+)://(\[^/\]+))($|/)(.*)} \{[regsub -all {\{|\}} $url {\\&}]\} match \
# 	      VFStok VFSpro VFSadd VFSextraRubish VFSpath]
# }
# proc IsVFS0 {url} {
#   return [uplevel regexp {^((\[^:\]+)://(\[^/\]+))($|/)(.*)}  $url match \
# 	      VFStok VFSpro VFSadd VFSextraRubish VFSpath]
# }
# The subst get around the attempt to look at $url as
# other than a string if it starts with $
proc IsVFS {url} {
  return [uplevel [subst -novariables -nocommands\
		       [list regexp {^((\[^:\]+)://(\[^/ \]+))($|/| )(.*)}\
			    $url match VFStok VFSpro VFSadd VFSextraRubish VFSpath]]]
}

# This command splits a URL (much as split) and returns a list
# of the parts. Missing parts are returned as nil ({}).
# The parts are: protocol, user, address, port and path

proc URLsplit {url} {
  if {![IsVFS $url]} {return {}}
  regexp {((.*)@)?([^:]*)?(:(.*))?} $VFSadd m ux user add px port
  return [list $VFSpro $user $add $port $VFSpath]
}
# just the address split...
proc VFSaddrSplit {VFSadd} {
  regexp {((.*)@)?([^:]*)?(:(.*))?} $VFSadd m ux user add px port
  return [list $user $add $port]
}

# Given a list of protocol user host and port this routine
# passes back the best matching option
# string from the config(vfs,helptable), or nil ({}) if none.
# The caller may also call with {*}[URLsplit $URL] to get the
# component parts of the address
#
proc VFShelpOptions {args} {
  lassign $args thisPro thisUser thisHost thisPort
  set strength -1
  set options {}
  
  foreach ent $::config(vfs,helptable) {
    set opts [lassign $ent vfs]
    lassign [URLsplit $vfs] Pro User Host Port
    set str 0
    set fail 0
    foreach part {Pro User Host Port} {
      if {[set this$part] == [set $part]} {
	incr str
      } else {
	if {[set $part] != {}} {
	  incr fail
	  break
	}
      }
    }
    if {$fail} {continue}
    
    # This qualifies. Is it strong enough?
    if {$str > $strength} {
      set strength $str
      set options $opts
    }
  }
  return $options
}

# URL normalize removes "." and ".." from the path
# while preserving the URL front end.
# For a local file it will also resolve soft links (same
# as file norm.
# This is set to work with any file path, VFS/URL or not
# NOTE: On MSW file norm wants to put the 'pwd' in front of
#       file names that start with "/". To prevent this we
#       put a volume name (a:) in front and then strip it
#       after 'file norm' is done. The use of 'a:' for this
#       should also prevent attempts to find softlinks.

# URL join does the "file" join for URLs and normal files
# in addition it removes the "." and ".."s

# URL directory does "file" directory on URLs and normal file names
# error on others

# The normalizeVFSPath is a bit of code to save coding the routine
# in three places. We also need slightly different code for MSW and *nix

# WARNNING ## WARNNING ## WARNNING ## WARNNING #
# We have a deep distrust of the 'join' option in the URL command below.
# The tcl 'file join' treats a leading "~" as the stare of an absolute path
# which it then tries to dereference (usually failing).
# For filerunner, we never want to join more than two items, it is much safer
# to do it manually.  The code in 'normalizeVFSPath' below works because 'IsVFS'
# puts a '/' in front of the VFSpath.
# The 'normalize' option has the same problem if the argument starts with '~'.
# If we know it is relative we should add the path, i.e. [pwd]/ or ./ if it is
# the cwd...
if {0} {
  if {$::MSW } {
    proc normalizeVFSPath {path} {
      upvar VFStok VFStok
      if {$path in {.  {}}} {return $VFStok}
      frputs VFStok path
      return $VFStok/[string range [file norm a:/$path] 2 end]
    }
  } else {
    proc normalizeVFSPath {path} {
      if {$path in {.  {}}} {return $VFStok}
      frputs VFStok path
      if {[string index $path 0] == "/"} {
	return $VFStok/[file norm $path]
      } else {
	return $VFStok[file norm /$path]
      }
    }
  }
} else {
  # here is a normalize that takes care of excess '/'s and '.' and '..'
  # on VFS we can not run down links here...
  proc normalizeVFSPath {path} {
    upvar VFStok VFStok
    set new {}
    foreach part [split $path /] {
      if {$part == "." || ($part == {} && $new != {})} {continue}
      if {$part == ".."} {
	if {[lindex $new end] != {}} {
	  set new [lreplace $new end end]
	}
	continue
      }
      if {[string match -nocase {[a-z]:} $part]} {
	set new {}
      }
      lappend new $part
    }
    frputs #5 #4 #3 #2 new VFStok "[llength $new] "
    if {$new == {{}} || $new == "${VFStok}///"} {return $VFStok//}
    return $VFStok/[join $new "/"]
  }
}
	  

proc URL {option URL args} {
  set pos {dirname normalize join}
  set opt [lsearch -glob -all -inline $pos "$option*"]
  if {$args != {} && $opt != "join" && $opt != {}} {
    return -code error "Too many arguments. Should be \"URL $opt url\"."
  }
  switch -exact $opt[IsVFS $URL] {
    normalize1 {return [normalizeVFSPath $VFSpath]}
    normalize0 {return [file norm $URL]}
    join1      {return [normalizeVFSPath [file join $VFSpath {*}$args]]}
    join0      {
      set tmp [file join $URL {*}$args]
      if {$URL == {}} {return $tmp}
      return [file norm $tmp]
    }
    dirname1   {return [normalizeVFSPath [file dir $VFSpath]]}
    dirname0   {return [file dir $URL]}
    1           -
    0           -
    default    {return -code error\
		    "URL: unrecognized option \"$option\", not one of \"$pos\""}
  }  
}
#
# Here we provide a VFS global statment
#
proc VFSglobal {args} {
  # We assume that the caller has a defined URL or VFStok in his local space
  uplevel {if {![info exists VFStok]} {IsVFS $URL}}
  uplevel [list foreach arg $args {
    upvar #0 ::VFSvars::($VFStok,$arg) $arg
  }]
}

# Exploring various ways to build a script with a \; in it. This is one
# of the few that work... WE leave here for education and recall...
# proc VFSglobalt {args} {
#   # We assume that the caller has a defined URL or VFStok in his local space
#   append s expr { {[info exists VFStok] ? {} : [IsVFS $URL]} ;foreach arg }
#   append s "$args" { {
#     upvar #0 ::VFSvars::($VFStok,$arg) $arg
#   }}
#   uplevel $s
# }

# Here is a name space to keep more global stuff in.

namespace eval VFSvars {
  # For full VFS all the following are required. For a VFS that is mapped
  # to file space, only open and close are required.

  # The way to think about this is to consider a read only file system
  variable requiredProcs [list open cd isdir pwd get close live]
			  
  variable eitherOrProcs [list {list listF}]
  variable optionalProcs [list rename delete mkdir rmdir chmod chown list listF\
			      put copy link command search debug menu]
  variable binaryEntries [list RcopyOk rmdirEmpty]

  # The defaultPwd is an array containing the working directories
  # that an open puts the VFS in when no path is requested.
  # It is maintained and used by the VFScheckSyntax routine in the
  # 'frNavagate.tcl' module. The array is indexed by the VFStok.
  variable defaultPwd
  variable open
  variable cache
  variable cacheCount 0
  variable timeout
  variable vfsIsUpWait
  variable afterId

  proc VFSopenSet {VFStok arg} {
    variable timeout
    set timeout($VFStok) 10000
    foreach {opt value} $arg {
      set tar [lsearch -glob -all -inline timeout "$opt*"]
      if {$tar == {}} {continue}
      if {[llength $tar] != 1} {
	error "VFSvars: option ($opt) must be timeout"
      }
      set timeout($VFStok) $value
      break
    }
    set afterId($VFStok) [after $timeout($VFStok) {}]
 
  }

  proc VFSnoCommand {cmd URL args} {
    # If we are here there was and attempt to call a missing proc
    IsVFS $URL
    return -code error "VFS \"$VFSpro\" does not support the $cmd function."
  }

  # The VFSsetCommands proc a) insures required commands are present and
  # b) Sets any missing commands to point the the VFSnoCommand which
  # generates a rather standard error message.
  
  proc VFSsetCommands {} {
    variable requiredProcs 
    variable optionalProcs 
    variable binaryEntries
    # First check for the min command set
    uplevel {
      foreach reqProc $::VFSvars::requiredProcs {
	if {![info exists commands($reqProc)]} {
	  lappend missing $reqProc
	}
      }
      foreach eithOr $::VFSvars::eitherOrProcs {
	lassign $eithOr a b
	if {![info exists commands($a)] && ![info exists commands($b)]} {
	  lappend missing "$a or $b"
	}
      }
      if {[info exists missing]} {
	if {[info exists commands(close)] && [info procs $commands(close)] != {}} {
	  uplevel [list {*}$commands(close) $VFStok]
	}
	error "Package VFS$VFSpro is missing the following commands: $missing"
      }
      # If we are still here, set up the missing commands
      foreach opProc $::VFSvars::optionalProcs {
      	if {![info exists commands($opProc)]} {
      	  set commands($opProc) [list ::VFSvars::VFSnoCommand $opProc]
      	}
      }
      foreach opProc $::VFSvars::binaryEntries {
	if {![info exists commands($opProc)]} {
	  set commands($opProc) 0
	}
      }
    }
  }

  # VFScheckNotMapped
  # VFScheckNotMapped URL This routine insures that the given URL is an open
  #                       URL and that it is not mapped. This is a local routine
  # Note: this code depends on the URL existing on the callers stack as "URL"
  # If it looks open, a test to see if the link is up is made, conditioned on
  # time since last request and if it is not canceled by an "arg" of ! {}

  proc VFScheckNotMapped {args} {
    variable open
    variable vfsIsUpWait
    variable afterId
    variable timeout
    variable temp $args

    uplevel {
      if {![IsVFS $URL] ||\
	      ! [info exists ::VFSvars::open($VFStok)] ||\
	      $::VFSvars::open($VFStok)!= $VFStok} {
	error "$URL is not an open VFS URL"
      }
      # Check if this access is beyond the timeout since the last one
      # We pass args through VFSvars::temp since it is not in scope
      # (nor is namespace current usable)
      if {[catch {after info $::VFSvars::afterId($VFStok)}] != 0 &&\
	      $::VFSvars::temp == {}} {
	set ::VFSvars::afterId($VFStok)\
	    [after 10000 [list set VFSvars::vfsIsUpWait($VFStok) 10]]
	after idle [list VFSvars::VFSisLinkUpTest\
			$VFStok [set ::VFS[set VFSpro]::commands(live)]]
	vwait ::VFSvars::vfsIsUpWait($VFStok)
	after cancel $::VFSvars::afterId($VFStok)
	lassign $VFSvars::vfsIsUpWait($VFStok) r ret
	if {$r != 0 || $ret == {}} {
	  # Link appears to be down...
	  frputs r ret
	  eval [list {*}[set ::VFS[set VFSpro]::commands(reopen)] $VFStok]
	}
      }
      after cancel $::VFSvars::afterId($VFStok)
      set ::VFSvars::afterId($VFStok) [after $VFSvars::timeout($VFStok) {}]
    }
  }

  # This is the after code used above
  proc VFSisLinkUpTest {VFStok live} {
    variable vfsIsUpWait
    variable command
     
    set r [catch {eval [list {*}$live $VFStok]} ret]
    set ::VFSvars::vfsIsUpWait($VFStok) [list $r $ret]
    frputs r ret
  }
  
  # Cache management code ===========================================
  proc VFS_WriteCache { key data } {
    variable cache
    variable cachet
    variable cacheTimeOut
    variable cacheCount
    
    global config
    set cache($key) $data
    set cachet($key) [incr cacheCount]
    #
    # It is not clear, given the timeout on entries, that this is needed
    if {[array size cache] > $config(vfs,cache,maxentries)} {
      # prunning time
      set low $cacheCount
      foreach ent [array names cachet] {
	if {$cachet($ent) < $low} {
	  set low $cachet($ent)
	  set an $ent
	}
      }
      unset cachet($an)
      unset cache($an)
    }
    # End of questionable prune code....
    if { [info exists  cacheTimeOut]} {
      after cancel $cacheTimeOut
    }
    # Lets try 1.5 min. (90000)
    set cacheTimeOut [after 90000 [namespace current]::VFS_InvalidateCache]
  }


  proc VFS_ReadCache { key} {
    variable cache
    variable cachet
    variable cacheCount

    if {[info exist cache($key)]} {
      set cachet($key) [incr cacheCount]
      return $cache($key)
    }
    return 0
  }

  proc VFS_InvalidateCache {{URL ""}} {
    variable cache
    variable cachet
    variable cacheCount
    variable cacheTimeOut

    if {$URL == ""} {
      array unset cache
      array unset cachet
      set cacheCount 0
      
      if { [info exists cacheTimeOut]} {
	after cancel $cacheTimeOut
	unset cacheTimeOut
      }
      return
    }
    # If we are here it is a targeted cache delete.
    # we cheat a little and go into the details of
    # the the entries to pick up the IsDir entries
    # We also need to not get tripped up on special
    # chars in the file/dir names.
    #
    # foreach name [array names cache -regexp "^(Dir:)?$URL.*$"] {}
    foreach name [array names cache -glob\
		      "*[regsub -all {\*|\?|\[|\]} $URL {\\&}]*"] {
       unset cache($name)
      unset cachet($name)
      frputs URL
    }
  }
#============================ End of Cache code  ================
}

#=================== End of NameSpace VFSvars ======[::VFSvars::VFS_ReadCache $URL]====

# # This routine will verify that the link is up and if not will try to
# # reopen it. If all that fails and error is thrown.
# proc VFSisLinkUp {$VFStok} {
#   frputs
#   # This gets control back if "live" hangs
#   set afterId [after 10000 {set ::vfsIsUpWait 10}]
#   after idle {VFSisLinkUpTest $VFStok}
#   after cancel $afterId
#   vwait ::vfsIsUpWait

#   lassign $::vfsIsUpWait r ret
#   if {$r != 0 || $ret == {}} {
#     # Link appears to be down...
#     frputs 
#     uplevel [list {*}$commands(reopen) $VFStok]
#   }
# }
# ====================================================
#
# The interface:
# In the following discussion the "real" code is that which is in the VFS
# package we are calling. 
#
# VFSopen URL options
# open (or Mount) For things like ftp this may mean contacting another system
#                 to extablish a connection. The open request requires a URL and
#                 a user as well as other possible options.

#                 The password is NOT passed, but rather obtained by the callee
#                 using the "password Locker" interface. This allows us to keep
#                 all passwords in one place and to manage them else where in
#                 encrypted form. It is recommended that callees not (in general)
#                 save passwords, but rather get them, use them and erase
#                 (or at least obscure) the local copy.

#        options  is a list of doublets of form "<option> <value>"
#                 expected options are:
#                 user      (may also be in the URL)
#                 port      (may also be in the URL)
#                 proxy     if an indirect connection
#                 abortflag global variable that, if set, will abort the connection
#                 timeout   how long to wait for an action in seconds
#                 log       log function for progress reports
#                 timefmt   format to use for time in progress reports
#                 debug     bool, true if debugging

# The open routine open should return {} or a corrected URL.
#

#  Comment: The following would appear to be an idea not used. Local VFS
#           and their management is now the job of cmdMount and friends.

#                 If the VFS is to be access with a <local root> (i.e. a tmp
#                 area on the local disk) the disk address of this area is
#                 to be returned by the "real' open.
#                 This code will put this address in a safe
#                 place.

#                 (For VFS protocols such as zip, the file to be
#                 mounted will be in the current working directory.)
#                 In the case of a <local root> the
#                 VFS implimtation should supply only the 'open' and 'close'
#                 routines.

proc VFSopen {URL args} {
  if {![IsVFS $URL]} {
    error "$URL is not a VFS URL"
  }
  set r [catch "package require VFS$VFSpro" er]
  if {$r != 0} {
    error "Failed to find VFS$VFSpro: $er"
  }
  if {! [info exists ::VFS${VFSpro}::commands(open)]} {
    error "VFS$VFSpro does not define VFS${VFSpro}::commands(open)"
  }
  # Well, it looks like we might be able to open/mount this thing...
  global ::VFS[set VFSpro]::commands
  frputs commands(open)
  set rtn [eval [list {*}$commands(open) $URL $args]]
  IsVFS $rtn
  set ::VFSvars::open($VFStok) $VFStok
  ::VFSvars::VFSsetCommands
  # Set our local vars
  VFSvars::VFSopenSet $VFStok [concat {*}$args]
  return [expr {$rtn == {} ? 1 : $rtn}]
}

# VFS supports command
# VFSsupport URL command  Returns true if VFS is open and supports "command"
# This 
proc VFSsupports {URL command} {
  VFSvars::VFScheckNotMapped
  global ::VFS[set VFSpro]::commands
  return [expr {[info exists commands($command)] && \
		    [lindex $commands($command) 0] ni {0 "::VFSvars::VFSnoCommand"}}]
}

# VFS management code:
# VFSisOpen URL   returns 0 if no open flag, else the corrected VFStok or
# what ever the vfs command returned.
#
proc VFSisOpen {URL} {
  if {[IsVFS $URL] && \
	  [info exists ::VFSvars::open($VFStok)] && \
	  $::VFSvars::open($VFStok) != 0} {
    return $::VFSvars::open($VFStok)
  } else {
    return 0
  }
}

# It is unclear if the live routine should be exported beyond its
# use in VFScheckNotMapped

# VFSlive URL
# live VFStok      This should insure that the vfs is
#                 still mounted or open and ready to handle requests.
#                 If it is not, this function should make every effort to
#                 restore the connection.  Failure here will close/unmount
#                 the connection. If this command fails to verify that the
#                 connetion is alive and well, it should return true. If
#                 a connection is closed/unmounted this should return false.
#                 If VFSlive ever returns false, the "reopen" routine must
#                 be supplied.
# proc VFSlive {URL} {
#   global ::VFS[set VFSpro]::commands
#   return [uplevel [list {*}$commands(live) $VFStok]]
# }

# VFScd URL
# cd VFStok dir     Change the directory to path. True if successful, false if not.
proc VFScd {URL} {
  VFSvars::VFScheckNotMapped
  global ::VFS[set VFSpro]::commands
  frputs URL VFSpath
  return [eval [list {*}$commands(cd) $VFStok $VFSpath]]
}

# VFSrename URL oldname newname dirFlag
# Rename VFStok new    Rename a file or directory (possibly implies move) True if
#                         successful, false if not. "dirFlag" should be true if
#                         the call is renaming a directory (helps us manage
#                         the cache).
proc VFSrename {URL new dir} {
  VFSvars::VFScheckNotMapped
  global ::VFS[set VFSpro]::commands
  if {$dir} {
    # if a dir, get the whole tree...
    ::VFSvars::VFS_InvalidateCache
  } else {
    # a file, just its dir(s)
    ::VFSvars::VFS_InvalidateCache [URL dir $URL]
    ::VFSvars::VFS_InvalidateCache [URL dir $VFStok/$new]
  }
  set rtn [uplevel [list {*}$commands(rename) $VFStok $VFSpath $new]]
  return $rtn
}

# VFSdelete URL
# Delete          Delete a file. True if successful, false if not.
proc VFSdelete {URL} {
  VFSvars::VFScheckNotMapped
  ::VFSvars::VFS_InvalidateCache $URL
  global ::VFS[set VFSpro]::commands
  return [uplevel [list {*}$commands(delete) $VFStok $VFSpath]]
}

# VFSmkdir URL
# Make Directory  Makes a new directory. True if successful, false if not.
#                 Note: directory is part of the URL

proc VFSmkdir {URL} {
  VFSvars::VFScheckNotMapped
  ::VFSvars::VFS_InvalidateCache [URL dir $URL]
  global ::VFS[set VFSpro]::commands
  return [uplevel [list {*}$commands(mkdir) $VFStok $VFSpath]]
}

# VFSrmdir URL
# rmdir              (open question on if the directory is empty. In general filerunner
#                    assumes we can delete a directory and every thing in it with
#                    just one call. OTH ftp can not do that and so we have a
#                    recursive function to do just this. So, we need an option
#                    flag here to indicate if this VFS can do the full delete.
#                    directory is indicated by the path part of the URL
proc VFSrmdir {URL} {
  VFSvars::VFScheckNotMapped
  ::VFSvars::VFS_InvalidateCache $URL
  ::VFSvars::VFS_InvalidateCache [URL dirname $URL]
  global ::VFS[set VFSpro]::commands
  return [uplevel [list {*}$commands(rmdir) $VFStok $VFSpath]]
}


# VFSisDir URL
# IsDir           Test if object is a directory. Either throw an
#                 error or return 0 if not. If it is return the
#                 actual dir name (assumes it may be a symbolic link)
#                 For the most part this is only called when a dir
#                 entry is a sym link.
#
proc VFSisDir {URL} {
  VFSvars::VFScheckNotMapped
  global ::VFS[set VFSpro]::commands
  set rtn [::VFSvars::VFS_ReadCache Dir:$URL]
  if {$rtn != 0} {
    return $rtn
  }
  set r [catch {{*}$commands(isdir) $VFStok $VFSpath} path]
  set rtn  [expr {$r == 0 && $path != 0 ? $path : {}}]
  ::VFSvars::VFS_WriteCache "Dir:$URL" $rtn
  return $rtn
}

# VFSpwd URL
# Pwd             Return the current working directory. It is assumed that
#                 this returns the true path to the WD even though
#                 the Cd command to get to it may have been via a link
#                 It would be "nice" if the return was a URL, but if not
#                 we fix it.
proc VFSpwd {URL} {
  VFSvars::VFScheckNotMapped
  global ::VFS[set VFSpro]::commands
  if {[IsVFS [set ret [uplevel [list {*}$commands(pwd) $VFStok]]]]} {
    return $ret
  }
  return [URL norm $VFStok/$ret]
}

# VFSchmod URL mode 
# chmod           Change mode of the object (file or directory) to mode.
#                 mode may inclued the recursion flag (formats TBD)
proc VFSchmod {URL mode} {
  VFSvars::VFScheckNotMapped
  ::VFSvars::VFS_InvalidateCache [URL dirname $URL]
  global ::VFS[set VFSpro]::commands
  return [uplevel [list {*}$commands(chmod) $VFStok $mode $VFSpath]]
}

# VFSchown URL owner
# chown           Change owner of the object (file or directory). owner may
#                 include a recursion flag
proc VFSchown {URL mode} {
  VFSvars::VFScheckNotMapped
  ::VFSvars::VFS_InvalidateCache $URL
  global ::VFS[set VFSpro]::commands
  return [uplevel [list {*}$commands(chown) $VFStok $mode $VFSpath]]
}

# VFSlist URL showall
# list            Return a list of files and their attributes for the current directory.
#                 Currently filerunner process several versions of this list.
#                 1.) Those provided by 'glob' and 'file stat'
#                 2.) Those provided by 'ftp'
#                 3.) Those provided by 'sftp' (which, I think, is from *nix 'ls')
#                 4.) The *nix 'ls' command.
#                 5.) The MS windows 'cmd dir' command (with most of its options)
# WARNNING "list" is passed the URL, not the VFStok. For the VFSglobal to work
#           correctly this NUST be called URL by the VFSlist/VFSlistF handler.
proc VFSlist {URL showall} {
  frputs URL showall
  VFSvars::VFScheckNotMapped
  LogStatusOnly "Reading VFS directory $URL"
  set result [::VFSvars::VFS_ReadCache $URL]
  if {$result != 0} {
    LogStatusOnly "done (found in cache)"
    return $result
  }
  global ::VFS[set VFSpro]::commands
  set result [uplevel [list {*}$commands(list) $URL $showall]]
  ::VFSvars::VFS_WriteCache $URL $result
  LogStatusOnly "done"
  return $result
}

# VFSlistF URL args
# list            Return a list of files and their attributes for the current directory.
#                 This is an optional list request. Either VFSlist or VFSlistF must
#                 be supported. In this format the callee (the VFS code) supplies a
#                 dictionary of values for each field seperated into
#                 the various bits of info about the file.
#                 The dictionary MUST have values for:
#                 file        <files name>
#                 type <one of: {d n ld ln}>   d->directory n->file l->link
#                 -optional-
#                 size        <file/dir size in bytes>
#                 sec         <last modify time>
#                 og          <owner/group string>
#                 link        <link to string>
#                 nlinks      <number of links> refers to hard links
#                 flags       <a set of r/w flags per ls>
#                 args is a list of doublets that controls various things.
#                 at this time {showall bool} is defined.
# WARNNING "list" is passed the URL, not the VFStok. For the VFSglobal to work
#            correctly this NUST be called URL by the VFSlist/VFSlistF handler.
proc VFSlistF {URL args} {
  frputs URL showall
  VFSvars::VFScheckNotMapped
  LogStatusOnly "Reading VFS directory $URL"
  set result [::VFSvars::VFS_ReadCache $URL]
  if {$result != 0} {
    LogStatusOnly "done (found in cache)"
    return $result
  }
  global ::VFS[set VFSpro]::commands
  set result [uplevel [list {*}$commands(listF) $URL $args]]
  ::VFSvars::VFS_WriteCache $URL $result
  LogStatusOnly "done"
  return $result
}

# VFSgetFile URL lfile size ?resume?
# get             Get contents of named URL and put in lfile.
#                 size is the expected size (used in progress report)
#                 if resume is true and lfile exists, the call gets
#                 the remainder of the file. If the get fails it should
#                 throw an error.
proc VFSgetFile {URL lfile size args} {
  VFSvars::VFScheckNotMapped
  global ::VFS[set VFSpro]::commands
  return [uplevel [list {*}$commands(get) $VFStok \
		       $VFSpath $lfile $size {*}$args]]
}


# VFSputFile URL localFileName size
# put             Creates (optionally) and writes a file. 
#                 If the "put" fails it should throw an error.
proc VFSputFile {URL localFileName size} {
  VFSvars::VFScheckNotMapped
  ::VFSvars::VFS_InvalidateCache [URL dirname $URL]
  global ::VFS[set VFSpro]::commands
  return [uplevel \
	      [list {*}$commands(put) \
		   $VFStok $localFileName $VFSpath $size]]
}

# VFScopy URL fromfile tofile (fromfile may be a list)
# copy        Copy within the VFS.
#             Optional command. Throws error if not supported

proc VFScopy {URL fromFile toFile} {
  VFSvars::VFScheckNotMapped
  global ::VFS[set VFSpro]::commands
  if {![info exists commands(copy)]} {
    return -code error "VFS $VFStok does not support \"copy\""
  }
  if {[IsVFS $fromFile]} {
    set fromFile $VFSpath
  }
  if {[IsVFS $toFile]} {
    set toFile $VFSpath
  }
  ::VFSvars::VFS_InvalidateCache [URL norm $VFStok/[file dirname $toFile]]
  return [uplevel [list {*}$commands(copy) $VFStok $fromFile $toFile]]
}  

# VFSlink URL new opt (r or a, relative or absolute)
# link            Creates the symbolic link 'new' pointing to URL.

proc VFSlink {URL new opt} {
  VFSvars::VFScheckNotMapped
  ::VFSvars::VFS_InvalidateCache [URL dirname $new]
  global ::VFS[set VFSpro]::commands
  return [uplevel \
	      [list {*}$commands(link) $VFStok $VFSpath $new $opt]]
}


# VFScommand URL command
# command         Execute given command in the given vfs enviroment
#                 the command should be a list if not a single word...
proc VFScommand {URL command} {
  VFSvars::VFScheckNotMapped
  global ::VFS[set VFSpro]::commands
  if {![info exists commands(command)]} {
    return -code error "VFS $VFStok does not support \"command\""
  }
  return [uplevel [list {*}$commands(command) $VFStok $command]]
}

# VFSasyncCommand {URL command call_back_function}
# asyncCommand    Execute the given command using the call_back_funtion to
#                 report/return results.
#                 The call_back_function should be called with the
#                 first parameter being the unique 'handle' followed by
#                 <key> and ?<info>? parms where <key> and <info> are one of:
#                 <key>     <info>
#                 data      <data from the command as it arrives>
#                 done      <nil> command has completed
# Note: return data should have \n separating lines and \r(s) removed
# This command requires command <asyncKill> see below.

# The command must return a unique 'handle' which is used by the asyncKill command
# as well as the call_back_function.


proc VFSasyncCommand {URL command call_back_function} {
  VFSvars::VFScheckNotMapped
  global ::VFS[set VFSpro]::commands
  if {[info exists commands(asyncCommand)] && [info exists commands(asyncKill)]} {
    set r [catch {uplevel [list {*}$comands(asyncCommand)\
			       $VFStok $command $call_back_function]} handle]
    return [list $r $handle]
  } elseif {![info exists commands(command)]} {
    return -code error "VFS $VFStok does not support \"asyncCommand or command\""
  }
  # Ok, lets fake it...
  VFSglobal handle
  
  if {[incr handle] != 1} {
    incr handle -1
    return -code error "Only one async call per VFS allowed."
  }
  frputs command
  set myHandle $VFStok$handle
  set aId [after 1\
	       [list VFSasyncFake $VFStok $command $call_back_function $myHandle]]
  frputs ::DoProtLevel aId
  return [list 0 $myHandle]
}

proc VFSasyncFake {VFStok command call_back myHandle} {
  realWaitForIdle
  VFSglobal handle
  frputs ::DoProtLevel
  while { [incr failsafe] < 200000 && $::DoProtLevel > 0} {
    realWaitForIdle
  }
  frputs failsafe
  if {$failsafe >= 200000} {
    set handle 0
    return -code error "Initializing comand still running after waiting."
  }
  set data [VFScommand $VFStok $command]
  frputs command data
  $call_back $myHandle {data} $data
  $call_back $myHandle {done}
  set handle 0
}

proc VFSasyncKill {URL thisHandle} {
  VFSvars::VFScheckNotMapped
  global ::VFS[set VFSpro]::commands

  if {![info exists commands(asyncKill)]} {
    # Well this is messy but it should work
    VFSglobal handle
    if {![info exists handle] || $handle == 0} {return}
    incr ::glob(abortcmd)
    #VFS $URL command "\x03"
    #VFSclose $VFStok
    set handle 0
    #uplevel [list {*}$commands(reopen) $VFStok]
    return
  }
  return [uplevel [list {*}$commands(asyncKill) $VFStok $handle]]
}

# VFSclose URL
# close           Disconnect and close the VFS. For things like ftp this just
#                 closes the connection. For data containers such as tar or zip
#                 if the mount is not read only, this will mean producing the
#                 updated container.
#                 We also remove the open entry and any VFS globals.
proc VFSclose {URL} {
  VFSvars::VFScheckNotMapped
  global ::VFS[set VFSpro]::commands
  set rtn [uplevel [list {*}$commands(close) $VFStok ]]
  array unset ::VFSvars::open $VFStok
  array unset ::VFSvars:: $VFStok,*
  return $rtn
}

# VFSmenu          This command (if it exists) should pass back a set of menu
#                  entries. This should be a list menu entries to build a menu
#                  to access and/or modify various items in the given VFS. When
#                  the "Etc" menu button is pressed filerunner makes this call
#                  to get items to put in that menu. The list items are used
#                  in a <pathname add> command to build the menu entry.
#                  Example: [list -label foois -command VFSfoo::fooiscmd]
#                  each element in the list is taken as a new add menu item
#                  This command is optional. 

#                  filerunner will do its own interrogation of the remote system
#                  via the 'command' function to set up the 'find' 'locate'
#                  and 'touch' (create an empty file) entries.

#                  If there is something other than find or locate to do searchs
#                  you can put an entry here to call it. (But note that the
#                  find and locate calls have dialog windows in front of them.)
#

proc VFSmenu {URL inst} {
  VFSvars::VFScheckNotMapped
  global ::VFS[set VFSpro]::commands
  return [uplevel [list {*}$commands(menu) $VFStok $inst]]
}

# VFSsearch URL file  (optional, throws error if not supported)
# search          searches for a file named file (with possible wild cards)
proc VFSsearch {URL file} {
  VFSvars::VFScheckNotMapped
  global ::VFS[set VFSpro]::commands
  if {[info exist commands(search)]} {
    return  [uplevel [list {*}$commands(search) $VFStok $file]]
  } else {
    return -code error "Search command not supported by ${VFSpro} VFS"
  }
}
# VFSdebug URL bool  1 sets 0 turns off (optional, ignore if not supported)
#                    enable/disenable debug messages
proc VFSdebug {URL bool} {
  VFSvars::VFScheckNotMapped 0
  global ::VFS[set VFSpro]::commands
  if {[info exist commands(debug)]} {
    return  [uplevel [list {*}$commands(debug) $VFStok $bool]]
  }
}

proc VFSRcopyOk {URL} {
  VFSvars::VFScheckNotMapped 0
  global ::VFS[set VFSpro]::commands
  return $commands(RcopyOk)
}

proc VFSrmdirEmpty {URL} {
  VFSvars::VFScheckNotMapped 0
  global ::VFS[set VFSpro]::commands
  return $commands(rmdirEmpty)
}
# VFSvolumes      This command is called if an attempt to open nil or "/" fails
#                 On MSW systems the 'volume' list is a list of volumes on the system.
#                 To eliminate special casing "pwd {}" and cd /D "" and the issues
#                 they engender, the system will attempt to call "VFSvolumes" to get
#                 a list of volumes. This call likely is only useful on MSW systems
#                 and is optional in all cases.

proc VFSvolumes {URL} {
  VFSvars::VFScheckNotMapped
  global ::VFS[set VFSpro]::commands
  if {[info exist commands(volumes)]} {
    return  [uplevel [list {*}$commands(volumes) $VFStok]]
  } else {
    return -code error "Volumes command not supported by ${VFSpro} VFS"
  }
}

# Each vfs will also have its own perconnection data area and should be coded such
# that, within data limits, as many connections as desired may be opened at the
# same time. The system (i.e. this code) provides a cache for the List command.
#
# Some vfs' will use scratch disc space to spread out components of their containers.
# This space should be released when the container is Closed/Unmounted.

# Filerunner can distinguish between a display directory and a working directory
# such that, for example zip://<path>.zip/ is displayed while /temp/zipXYZ/ is used.
#
# What is "VFStok"? VFStok is an intersection of a type of VFS and its root address.
# It is the VFSs URL without the path, and it has exactly that format.
#
# Within the filerunner VFS the token is the name of an array with the following
# entries (some of which may not exist):
#
# open  if this does not exist the VFS is not mounted or open
#       else, the access path to the VFS.  For FTP SFTP and such this will be
#       the same as the "token". For others it may be a tmp area where the
#       local expanded VFS is (or will be) located.
# other other entries in this array are used by the given 'protocol' to keep
#       track of a given instance of open or mount.
#
# The commands for each protocol are kept in an array named:
# VFS<protocol>::commands
# For example VFSftp, VFSsftp and so on.
# Each supported protocol action command will be in this array. Unsupported commands
# should have no entry at all. (Attempts to use these commands will be trapped
# and return a standard error.)

# Making a VFS visabile to filerunner.  Filerunner uses the "package names" command
# to find VFS systems. A package named "VFS<protocol>" defines to filerunner that
# a VFS set of code is available for that <protocol>. When the given package is
# "required" it should define the array VFS<protocol>::commands. This array should
# have the following entries:
#
# open reopen live cd delete mkdir rmdir rmdirEmpty isdir pwd chmod chown list
# get put RcopyOk rename link command close and, optionally copy.
#
# RcopyOk indicates if the VFS can handle recursive copys of directries. It uses
#         two bits bit 0 for read or get and bit 1 for write or put. Thus:
#         0 Neither read nor write recursive is supported
#         1 read recursive is supported, write is not
#         2 write recursive is supported, read is not
#         3 both read and write recursive is supported.

# rmdirEmpty should be true if and only if the VFS can rmdir non-empty dirs.
#
# of these rmdirEmpty and RcopyOk should be binary (true or false). All the rest
# should be command names that preform the indicated function. Except for live
# and reopen, these are defined above by example (see above).

# LINK UP AND RECOVERY CODE

# This file (frVFS.tcl) provides a "link is up" verification when ever a request is
# made more that "timeout" time after the last one. This is done by calling "live"
# and if it fails, calling "reopen". For links that are always closed after a
# transaction, it is recommended that "live" return immeadiately with an up
# return. In such a case reopen will never be called by this code.

# The "live" routine should take one argument (VFStok) and return <something> if the
# link is up. If the link is not up, it should either throw an error or return {}.
# We are defining "live" such that it may be most any access. VFSftp uses the "pwd"
# command.
#
# The "reopen" command also take one argument (VFStok). It will be called if "live"
# either fails or does not return with in "timeout" time (currently this is 10
# seconds).
# Its job is a.) close the given link and attempt to reopen it. This means that open
# should save what ever is needed to do this reopen. The reopen routine should
# also recover the current working dir.

#

# In addition, the "package require" should define any of these commands that need
# defineing (it is possible that some of these commands map to commands outside
# of the package such as read or write and so do not need defining).



