]> git.cworth.org Git - sup/blobdiff - lib/sup/mbox/ssh-file.rb
improve mime-view and mime-decode hook documentation
[sup] / lib / sup / mbox / ssh-file.rb
index 9e91d709101d59492248dba45184ab6a87d883e6..d47463692e07bedce205e4c3527f87f345c528c6 100644 (file)
@@ -66,6 +66,7 @@ class Buffer
     x = @buf.index(what, start - @start)
     x.nil? ? nil : x + @start
   end
+
   def rindex what, start=0
     x = @buf.rindex(what, start - @start)
     x.nil? ? nil : x + @start
@@ -75,13 +76,28 @@ class Buffer
   def to_s; empty? ? "<empty>" : "[#{start}, #{endd})"; end # for debugging
 end
 
+## sharing a ssh connection to one machines between sources seems to
+## create lots of broken situations: commands returning bizarre (large
+## positive integer) return codes despite working; commands
+## occasionally not working, etc. i suspect this is because of the
+## fragile nature of the ssh syncshell. 
+##
+## at any rate, we now open up one ssh connection per file, which is
+## probably silly in the extreme case.
+
 ## the file-like interface to a remote file
 class SSHFile
   MAX_BUF_SIZE = 1024 * 1024 # bytes
-  MAX_TRANSFER_SIZE = 1024 * 64
+  MAX_TRANSFER_SIZE = 1024 * 128
   REASONABLE_TRANSFER_SIZE = 1024 * 32
   SIZE_CHECK_INTERVAL = 60 * 1 # seconds
 
+  ## upon these errors we'll try to rereconnect a few times
+  RECOVERABLE_ERRORS = [ Errno::EPIPE, Errno::ETIMEDOUT ]
+
+  @@shells = {}
+  @@shells_mutex = Mutex.new
+
   def initialize host, fn, ssh_opts={}
     @buf = Buffer.new
     @host = host
@@ -90,42 +106,15 @@ class SSHFile
     @file_size = nil
     @offset = 0
     @say_id = nil
-    @broken_msg = nil
+    @shell = nil
+    @shell_mutex = nil
+    @buf_mutex = Mutex.new
   end
 
-  def broken?; !@broken_msg.nil?; end
-
-  def say s
-    @say_id = BufferManager.say s, @say_id if BufferManager.instantiated?
-    Redwood::log s
-  end
-  private :say
-
-  def shutup
-    BufferManager.clear @say_id if BufferManager.instantiated?
-    @say_id = nil
-  end
+  def to_s; "mbox+ssh://#@host/#@fn"; end ## TODO: remove this EVILness
 
   def connect
-    return if @session
-    raise SSHFileError, @broken_msg if broken?
-
-    say "Opening SSH connection to #{@host}..."
-
-    begin
-      #raise SSHFileError, "simulated SSH file error"
-      #@session = Net::SSH.start @host, @ssh_opts
-      sleep 3
-      say "Starting SSH shell..."
-      # @shell = @session.shell.sync
-      sleep 3
-      say "Checking for #@fn..."
-      sleep 1
-      raise Errno::ENOENT, @fn
-      raise Errno::ENOENT, @fn unless @shell.test("-e #@fn").status == 0
-    ensure
-      shutup
-    end
+    do_remote nil
   end
 
   def eof?; @offset >= size; end
@@ -133,6 +122,7 @@ class SSHFile
   def seek loc; @offset = loc; end
   def tell; @offset; end
   def total; size; end
+  def path; @fn end
 
   def size
     if @file_size.nil? || (Time.now - @last_size_check) > SIZE_CHECK_INTERVAL
@@ -144,36 +134,81 @@ class SSHFile
 
   def gets
     return nil if eof?
-    make_buf_include @offset
-    expand_buf_forward while @buf.index("\n", @offset).nil? && @buf.endd < size
-    returning(@buf[@offset .. (@buf.index("\n", @offset) || -1)]) { |line| @offset += line.length }
+    @buf_mutex.synchronize do
+      make_buf_include @offset
+      expand_buf_forward while @buf.index("\n", @offset).nil? && @buf.endd < size
+      returning(@buf[@offset .. (@buf.index("\n", @offset) || -1)]) { |line| @offset += line.length }
+    end
   end
 
   def read n
     return nil if eof?
-    make_buf_include @offset, n
-    @buf[@offset ... (@offset += n)]
+    @buf_mutex.synchronize do
+      make_buf_include @offset, n
+      @buf[@offset ... (@offset += n)]
+    end
   end
 
 private
 
+  ## TODO: share this code with imap
+  def say s
+    @say_id = BufferManager.say s, @say_id if BufferManager.instantiated?
+    Redwood::log s
+  end
+
+  def shutup
+    BufferManager.clear @say_id if BufferManager.instantiated? && @say_id
+    @say_id = nil
+  end
+
+  def unsafe_connect
+    return if @shell
+
+    @key = [@host, @ssh_opts[:username]]
+    begin
+      @shell, @shell_mutex = @@shells_mutex.synchronize do
+        unless @@shells.member? @key
+          say "Opening SSH connection to #{@host} for #@fn..."
+          session = Net::SSH.start @host, @ssh_opts
+          say "Starting SSH shell..."
+          @@shells[@key] = [session.shell.sync, Mutex.new]
+        end
+        @@shells[@key]
+      end
+      
+      say "Checking for #@fn..."
+      @shell_mutex.synchronize { raise Errno::ENOENT, @fn unless @shell.test("-e #@fn").status == 0 }
+    ensure
+      shutup
+    end
+  end
+
   def do_remote cmd, expected_size=0
+    retries = 0
+    result = nil
+
     begin
-      retries = 0
-      connect
-      MBox::debug "sending command: #{cmd.inspect}"
-      begin
-        result = @shell.send_command cmd
-        raise SSHFileError, "Failure during remote command #{cmd.inspect}: #{result.stderr[0 .. 100]}" unless result.status == 0
-      rescue Net::SSH::Exception # these happen occasionally for no apparent reason. gotta love that nondeterminism!
-        retry if (retries += 1) < 3
-        raise
+      unsafe_connect
+      if cmd
+        # MBox::debug "sending command: #{cmd.inspect}"
+        result = @shell_mutex.synchronize { x = @shell.send_command cmd; sleep 0.25; x }
+        raise SSHFileError, "Failure during remote command #{cmd.inspect}: #{(result.stderr || result.stdout || "")[0 .. 100]}" unless result.status == 0
+      end
+      ## Net::SSH::Exceptions seem to happen every once in a while for
+      ## no good reason.
+    rescue Net::SSH::Exception, *RECOVERABLE_ERRORS
+      if (retries += 1) <= 3
+        @@shells_mutex.synchronize do
+          @shell = nil
+          @@shells[@key] = nil
+        end
+        retry
       end
-    rescue Net::SSH::Exception, SSHFileError, Errno::ENOENT => e
-      @broken_msg = e.message
       raise
     end
-    result.stdout
+
+    result.stdout if cmd
   end
 
   def get_bytes offset, size