]> git.cworth.org Git - sup/blobdiff - lib/sup/util.rb
Merge branch 'locking-refactor'
[sup] / lib / sup / util.rb
index c9ac70c2473fd706158db21704f3fc7bd594cde5..068ce6bad904c9012bdcd5d8883ac48a68e59976 100644 (file)
@@ -1,3 +1,102 @@
+require 'thread'
+require 'lockfile'
+require 'mime/types'
+require 'pathname'
+require 'set'
+
+## time for some monkeypatching!
+class Lockfile
+  def gen_lock_id
+    Hash[
+         'host' => "#{ Socket.gethostname }",
+         'pid' => "#{ Process.pid }",
+         'ppid' => "#{ Process.ppid }",
+         'time' => timestamp,
+         'pname' => $0,
+         'user' => ENV["USER"]
+        ]
+  end
+
+  def dump_lock_id lock_id = @lock_id
+      "host: %s\npid: %s\nppid: %s\ntime: %s\nuser: %s\npname: %s\n" %
+        lock_id.values_at('host','pid','ppid','time','user', 'pname')
+    end
+
+  def lockinfo_on_disk
+    h = load_lock_id IO.read(path)
+    h['mtime'] = File.mtime path
+    h['path'] = path
+    h
+  end
+
+  def touch_yourself; touch path end
+end
+
+class Pathname
+  def human_size
+    s =
+      begin
+        size
+      rescue SystemCallError
+        return "?"
+      end
+    s.to_human_size
+  end
+
+  def human_time
+    begin
+      ctime.strftime("%Y-%m-%d %H:%M")
+    rescue SystemCallError
+      "?"
+    end
+  end
+end
+
+## more monkeypatching!
+module RMail
+  class EncodingUnsupportedError < StandardError; end
+
+  class Message
+    def self.make_file_attachment fn
+      bfn = File.basename fn
+      t = MIME::Types.type_for(bfn).first || MIME::Types.type_for("exe").first
+      make_attachment IO.read(fn), t.content_type, t.encoding, bfn.to_s
+    end
+
+    def charset
+      if header.field?("content-type") && header.fetch("content-type") =~ /charset="?(.*?)"?(;|$)/i
+        $1
+      end
+    end
+
+    def self.make_attachment payload, mime_type, encoding, filename
+      a = Message.new
+      a.header.add "Content-Disposition", "attachment; filename=#{filename.inspect}"
+      a.header.add "Content-Type", "#{mime_type}; name=#{filename.inspect}"
+      a.header.add "Content-Transfer-Encoding", encoding if encoding
+      a.body =
+        case encoding
+        when "base64"
+          [payload].pack "m"
+        when "quoted-printable"
+          [payload].pack "M"
+        when "7bit", "8bit", nil
+          payload
+        else
+          raise EncodingUnsupportedError, encoding.inspect
+        end
+      a
+    end
+  end
+end
+
+class Range
+  ## only valid for integer ranges (unless I guess it's exclusive)
+  def size
+    last - first + (exclude_end? ? 0 : 1)
+  end
+end
+
 class Module
   def bool_reader *args
     args.each { |sym| class_eval %{ def #{sym}?; @#{sym}; end } }
@@ -9,7 +108,12 @@ class Module
   end
 
   def defer_all_other_method_calls_to obj
-    class_eval %{ def method_missing meth, *a, &b; @#{obj}.send meth, *a, &b; end }
+    class_eval %{
+      def method_missing meth, *a, &b; @#{obj}.send meth, *a, &b; end
+      def respond_to?(m, include_private = false)
+        @#{obj}.respond_to?(m, include_private)
+      end
+    }
   end
 end
 
@@ -25,25 +129,14 @@ class Object
     ret
   end
 
-  ## takes a value which it yields and then returns, so that code
-  ## like:
-  ##
-  ## x = expensive_operation
-  ## log "got #{x}"
-  ## x
-  ##
-  ## now becomes:
-  ##
-  ## with(expensive_operation) { |x| log "got #{x}" }
-  ##
-  ## i'm sure there's pithy comment i could make here about the
-  ## superiority of lisp, but fuck lisp.
+  ## "k combinator"
   def returning x; yield x; x; end
 
   ## clone of java-style whole-method synchronization
   ## assumes a @mutex variable
-  def synchronized *meth
-    meth.each do
+  ## TODO: clean up, try harder to avoid namespace collisions
+  def synchronized *methods
+    methods.each do |meth|
       class_eval <<-EOF
         alias unsynchronized_#{meth} #{meth}
         def #{meth}(*a, &b)
@@ -52,9 +145,45 @@ class Object
       EOF
     end
   end
+
+  def ignore_concurrent_calls *methods
+    methods.each do |meth|
+      mutex = "@__concurrent_protector_#{meth}"
+      flag = "@__concurrent_flag_#{meth}"
+      oldmeth = "__unprotected_#{meth}"
+      class_eval <<-EOF
+        alias #{oldmeth} #{meth}
+        def #{meth}(*a, &b)
+          #{mutex} = Mutex.new unless defined? #{mutex}
+          #{flag} = true unless defined? #{flag}
+          run = #{mutex}.synchronize do
+            if #{flag}
+              #{flag} = false
+              true
+            end
+          end
+          if run
+            ret = #{oldmeth}(*a, &b)
+            #{mutex}.synchronize { #{flag} = true }
+            ret
+          end
+        end
+      EOF
+    end
+  end
 end
 
 class String
+  ## nasty multibyte hack for ruby 1.8. if it's utf-8, split into chars using
+  ## the utf8 regex and count those. otherwise, use the byte length.
+  def display_length
+    if $encoding == "UTF-8"
+      scan(/./u).size
+    else
+      size
+    end
+  end
+
   def camel_to_hyphy
     self.gsub(/([a-z])([A-Z0-9])/, '\1-\2').downcase
   end
@@ -71,16 +200,70 @@ class String
     ret
   end
 
-  def ucfirst
-    self[0 .. 0].upcase + self[1 .. -1]
-  end
-
   ## a very complicated regex found on teh internets to split on
   ## commas, unless they occurr within double quotes.
   def split_on_commas
     split(/,\s*(?=(?:[^"]*"[^"]*")*(?![^"]*"))/)
   end
 
+  ## ok, here we do it the hard way. got to have a remainder for purposes of
+  ## tab-completing full email addresses
+  def split_on_commas_with_remainder
+    ret = []
+    state = :outstring
+    pos = 0
+    region_start = 0
+    while pos <= length
+      newpos = case state
+        when :escaped_instring, :escaped_outstring then pos
+        else index(/[,"\\]/, pos)
+      end
+
+      if newpos
+        char = self[newpos]
+      else
+        char = nil
+        newpos = length
+      end
+
+      case char
+      when ?"
+        state = case state
+          when :outstring then :instring
+          when :instring then :outstring
+          when :escaped_instring then :instring
+          when :escaped_outstring then :outstring
+        end
+      when ?,, nil
+        state = case state
+          when :outstring, :escaped_outstring then
+            ret << self[region_start ... newpos].gsub(/^\s+|\s+$/, "")
+            region_start = newpos + 1
+            :outstring
+          when :instring then :instring
+          when :escaped_instring then :instring
+        end
+      when ?\\
+        state = case state
+          when :instring then :escaped_instring
+          when :outstring then :escaped_outstring
+          when :escaped_instring then :instring
+          when :escaped_outstring then :outstring
+        end
+      end
+      pos = newpos + 1
+    end
+
+    remainder = case state
+      when :instring
+        self[region_start .. -1].gsub(/^\s+/, "")
+      else
+        nil
+      end
+
+    [ret, remainder]
+  end
+
   def wrap len
     ret = []
     s = self
@@ -100,6 +283,19 @@ class String
   def normalize_whitespace
     gsub(/\t/, "    ").gsub(/\r/, "")
   end
+
+  unless method_defined? :ord
+    def ord
+      self[0]
+    end
+  end
+
+  ## takes a list of words, and returns an array of symbols.  typically used in
+  ## Sup for translating Ferret's representation of a list of labels (a string)
+  ## to an array of label symbols.
+  ##
+  ## split_on will be passed to String#split, so you can leave this nil for space.
+  def to_set_of_symbols split_on=nil; Set.new split(split_on).map { |x| x.strip.intern } end
 end
 
 class Numeric
@@ -114,14 +310,21 @@ class Numeric
   end
 
   def in? range; range.member? self; end
+
+  def to_human_size
+    if self < 1024
+      to_s + "b"
+    elsif self < (1024 * 1024)
+      (self / 1024).to_s + "k"
+    elsif self < (1024 * 1024 * 1024)
+      (self / 1024 / 1024).to_s + "m"
+    else
+      (self / 1024 / 1024 / 1024).to_s + "g"
+    end
+  end
 end
 
 class Fixnum
-  def num_digits base=10
-    return 1 if self == 0
-    1 + (Math.log(self) / Math.log(10)).floor
-  end
-  
   def to_character
     if self < 128 && self >= 0
       chr
@@ -129,6 +332,20 @@ class Fixnum
       "<#{self}>"
     end
   end
+
+  ## hacking the english language
+  def pluralize s
+    to_s + " " +
+      if self == 1
+        s
+      else
+        if s =~ /(.*)y$/
+          $1 + "ies"
+        else
+          s + "s"
+        end
+      end
+  end
 end
 
 class Hash
@@ -174,6 +391,24 @@ module Enumerable
     end
     best
   end
+
+  ## returns the maximum shared prefix of an array of strings
+  ## optinally excluding a prefix
+  def shared_prefix caseless=false, exclude=""
+    return "" if empty?
+    prefix = ""
+    (0 ... first.length).each do |i|
+      c = (caseless ? first.downcase : first)[i]
+      break unless all? { |s| (caseless ? s.downcase : s)[i] == c }
+      next if exclude[i] == c
+      prefix += first[i].chr
+    end
+    prefix
+  end
+
+  def max_of
+    map { |e| yield e }.max
+  end
 end
 
 class Array
@@ -185,6 +420,9 @@ class Array
   def rest; self[1..-1]; end
 
   def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
+
+  def last= e; self[-1] = e end
+  def nonempty?; !empty? end
 end
 
 class Time
@@ -258,27 +496,166 @@ class Time
   end
 end
 
-## simple singleton module. far less complete and insane than the ruby
-## standard library one, but automatically forwards methods calls and
-## allows for constructors that take arguments.
+## simple singleton module. far less complete and insane than the ruby standard
+## library one, but it automatically forwards methods calls and allows for
+## constructors that take arguments.
 ##
-## You must have #initialize call "self.class.i_am_the_instance self"
-## at some point or everything will fail horribly
+## classes that inherit this can define initialize. however, you cannot call
+## .new on the class. To get the instance of the class, call .instance;
+## to create the instance, call init.
 module Singleton
   module ClassMethods
     def instance; @instance; end
     def instantiated?; defined?(@instance) && !@instance.nil?; end
+    def deinstantiate!; @instance = nil; end
     def method_missing meth, *a, &b
-      raise "no instance defined!" unless defined? @instance
+      raise "no #{name} instance defined in method call to #{meth}!" unless defined? @instance
+
+      ## if we've been deinstantiated, just drop all calls. this is
+      ## useful because threads that might be active during the
+      ## cleanup process (e.g. polling) would otherwise have to
+      ## special-case every call to a Singleton object
+      return nil if @instance.nil?
+
       @instance.send meth, *a, &b
     end
-    def i_am_the_instance o
+    def init *args
       raise "there can be only one! (instance)" if defined? @instance
-      @instance = o
+      @instance = new(*args)
     end
   end
 
   def self.included klass
+    klass.private_class_method :allocate, :new
     klass.extend ClassMethods
   end
 end
+
+## wraps an object. if it throws an exception, keeps a copy.
+class Recoverable
+  def initialize o
+    @o = o
+    @error = nil
+    @mutex = Mutex.new
+  end
+
+  attr_accessor :error
+
+  def clear_error!; @error = nil; end
+  def has_errors?; !@error.nil?; end
+
+  def method_missing m, *a, &b; __pass m, *a, &b end
+
+  def id; __pass :id; end
+  def to_s; __pass :to_s; end
+  def to_yaml x; __pass :to_yaml, x; end
+  def is_a? c; @o.is_a? c; end
+
+  def respond_to?(m, include_private=false)
+    @o.respond_to?(m, include_private)
+  end
+
+  def __pass m, *a, &b
+    begin
+      @o.send(m, *a, &b)
+    rescue Exception => e
+      @error ||= e
+      raise
+    end
+  end
+end
+
+## acts like a hash with an initialization block, but saves any
+## newly-created value even upon lookup.
+##
+## for example:
+##
+## class C
+##   attr_accessor :val
+##   def initialize; @val = 0 end
+## end
+## 
+## h = Hash.new { C.new }
+## h[:a].val # => 0
+## h[:a].val = 1
+## h[:a].val # => 0
+##
+## h2 = SavingHash.new { C.new }
+## h2[:a].val # => 0
+## h2[:a].val = 1
+## h2[:a].val # => 1
+##
+## important note: you REALLY want to use #member? to test existence,
+## because just checking h[anything] will always evaluate to true
+## (except for degenerate constructor blocks that return nil or false)
+class SavingHash
+  def initialize &b
+    @constructor = b
+    @hash = Hash.new
+  end
+
+  def [] k
+    @hash[k] ||= @constructor.call(k)
+  end
+
+  defer_all_other_method_calls_to :hash
+end
+
+class OrderedHash < Hash
+  alias_method :store, :[]=
+  alias_method :each_pair, :each
+  attr_reader :keys
+
+  def initialize *a
+    @keys = []
+    a.each { |k, v| self[k] = v }
+  end
+
+  def []= key, val
+    @keys << key unless member?(key)
+    super
+  end
+
+  def values; keys.map { |k| self[k] } end
+  def index key; @keys.index key end
+
+  def delete key
+    @keys.delete key
+    super
+  end
+
+  def each; @keys.each { |k| yield k, self[k] } end
+end
+
+## easy thread-safe class for determining who's the "winner" in a race (i.e.
+## first person to hit the finish line
+class FinishLine
+  def initialize
+    @m = Mutex.new
+    @over = false
+  end
+
+  def winner?
+    @m.synchronize { !@over && @over = true }
+  end
+end
+
+class Iconv
+  def self.easy_decode target, charset, text
+    return text if charset =~ /^(x-unknown|unknown[-_ ]?8bit|ascii[-_ ]?7[-_ ]?bit)$/i
+    charset = case charset
+      when /UTF[-_ ]?8/i then "utf-8"
+      when /(iso[-_ ])?latin[-_ ]?1$/i then "ISO-8859-1"
+      when /iso[-_ ]?8859[-_ ]?15/i then 'ISO-8859-15'
+      when /unicode[-_ ]1[-_ ]1[-_ ]utf[-_]7/i then "utf-7"
+      else charset
+    end
+
+    begin
+      Iconv.iconv(target + "//IGNORE", charset, text + " ").join[0 .. -2]
+    rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::InvalidCharacter, Iconv::IllegalSequence => e
+      warn "couldn't transcode text from #{charset} to #{target} (\"#{text[0 ... 20]}\"...) (got #{e.message}); using original as is"
+      text
+    end
+  end
+end