X-Git-Url: https://git.cworth.org/git?a=blobdiff_plain;f=lib%2Fsup%2Futil.rb;h=8f60cc43216979aaa207696324f117ba7025cbdd;hb=9716c862c1e2cd37ffbcb6b63be576ebe6ed9779;hp=46a10230982e6cb3348b8e92f6cca8c01dbd5a40;hpb=8220732408d9c0d77f1d6cb9c9986362c3bfa7f9;p=sup diff --git a/lib/sup/util.rb b/lib/sup/util.rb index 46a1023..8f60cc4 100644 --- a/lib/sup/util.rb +++ b/lib/sup/util.rb @@ -1,3 +1,100 @@ +require 'thread' +require 'lockfile' +require 'mime/types' +require 'pathname' + +## 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 + 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 } } @@ -7,6 +104,15 @@ class Module bool_reader(*args) bool_writer(*args) end + + def defer_all_other_method_calls_to obj + 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 class Object @@ -20,9 +126,62 @@ class Object end ret end + + ## "k combinator" + def returning x; yield x; x; end + + ## clone of java-style whole-method synchronization + ## assumes a @mutex variable + ## 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) + @mutex.synchronize { unsynchronized_#{meth}(*a, &b) } + end + 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 @@ -39,31 +198,100 @@ 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 while s.length > len cut = s[0 ... len].rindex(/\s/) if cut - ret << s[0 ... cut] + "\n" + ret << s[0 ... cut] s = s[(cut + 1) .. -1] else - ret << s[0 ... len] + "\n" + ret << s[0 ... len] s = s[len .. -1] end end ret << s end + + def normalize_whitespace + gsub(/\t/, " ").gsub(/\r/, "") + end + + if not defined? ord + def ord + self[0] + end + end + + ## takes a space-separated 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. + def symbolistize; split.map { |x| x.intern } end end class Numeric @@ -78,14 +306,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 @@ -93,6 +328,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 @@ -138,6 +387,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 @@ -149,18 +416,12 @@ class Array def rest; self[1..-1]; end def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end - - ## apparently uniq doesn't use ==. wtf. - def remove_successive_dupes - ret = [] - last = nil - each do |e| - unless e == last - ret << e - last = e - end - end - ret + + def last= e; self[-1] = e end + def nonempty?; !empty? end + + def to_set_of_symbols + map { |x| x.is_a?(Symbol) ? x : x.intern }.uniq end end @@ -197,16 +458,16 @@ class Time ["minute", 60], ["hour", 24], ["day", 7], - ["week", 4], # heh heh + ["week", 4.345], # heh heh ["month", 12], ["year", nil], ].argfind do |unit, size| - if diff <= 1 + if diff.round <= 1 "one #{unit}" - elsif size.nil? || diff < size - "#{diff} #{unit}s" + elsif size.nil? || diff.round < size + "#{diff.round} #{unit}s" else - diff = (diff / size.to_f).round + diff /= size.to_f false end end @@ -240,13 +501,21 @@ end ## 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 +## at some point or everything will fail horribly. 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 + + ## 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 @@ -259,3 +528,132 @@ module Singleton 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::IllegalSequence => e + Redwood::log "warning: error (#{e.class.name}) decoding text from #{charset} to #{target}: #{text[0 ... 20]}" + text + end + end +end