]> git.cworth.org Git - sup/blob - lib/sup/util.rb
minor code style tweaks and whitespace removals
[sup] / lib / sup / util.rb
1 require 'thread'
2 require 'lockfile'
3 require 'mime/types'
4 require 'pathname'
5 require 'set'
6
7 ## time for some monkeypatching!
8 class Lockfile
9   def gen_lock_id
10     Hash[
11          'host' => "#{ Socket.gethostname }",
12          'pid' => "#{ Process.pid }",
13          'ppid' => "#{ Process.ppid }",
14          'time' => timestamp,
15          'pname' => $0,
16          'user' => ENV["USER"]
17         ]
18   end
19
20   def dump_lock_id lock_id = @lock_id
21       "host: %s\npid: %s\nppid: %s\ntime: %s\nuser: %s\npname: %s\n" %
22         lock_id.values_at('host','pid','ppid','time','user', 'pname')
23     end
24
25   def lockinfo_on_disk
26     h = load_lock_id IO.read(path)
27     h['mtime'] = File.mtime path
28     h
29   end
30
31   def touch_yourself; touch path end
32 end
33
34 class Pathname
35   def human_size
36     s =
37       begin
38         size
39       rescue SystemCallError
40         return "?"
41       end
42     s.to_human_size
43   end
44
45   def human_time
46     begin
47       ctime.strftime("%Y-%m-%d %H:%M")
48     rescue SystemCallError
49       "?"
50     end
51   end
52 end
53
54 ## more monkeypatching!
55 module RMail
56   class EncodingUnsupportedError < StandardError; end
57
58   class Message
59     def self.make_file_attachment fn
60       bfn = File.basename fn
61       t = MIME::Types.type_for(bfn).first || MIME::Types.type_for("exe").first
62       make_attachment IO.read(fn), t.content_type, t.encoding, bfn.to_s
63     end
64
65     def charset
66       if header.field?("content-type") && header.fetch("content-type") =~ /charset="?(.*?)"?(;|$)/i
67         $1
68       end
69     end
70
71     def self.make_attachment payload, mime_type, encoding, filename
72       a = Message.new
73       a.header.add "Content-Disposition", "attachment; filename=#{filename.inspect}"
74       a.header.add "Content-Type", "#{mime_type}; name=#{filename.inspect}"
75       a.header.add "Content-Transfer-Encoding", encoding if encoding
76       a.body =
77         case encoding
78         when "base64"
79           [payload].pack "m"
80         when "quoted-printable"
81           [payload].pack "M"
82         when "7bit", "8bit", nil
83           payload
84         else
85           raise EncodingUnsupportedError, encoding.inspect
86         end
87       a
88     end
89   end
90 end
91
92 class Range
93   ## only valid for integer ranges (unless I guess it's exclusive)
94   def size 
95     last - first + (exclude_end? ? 0 : 1)
96   end
97 end
98
99 class Module
100   def bool_reader *args
101     args.each { |sym| class_eval %{ def #{sym}?; @#{sym}; end } }
102   end
103   def bool_writer *args; attr_writer(*args); end
104   def bool_accessor *args
105     bool_reader(*args)
106     bool_writer(*args)
107   end
108
109   def defer_all_other_method_calls_to obj
110     class_eval %{
111       def method_missing meth, *a, &b; @#{obj}.send meth, *a, &b; end
112       def respond_to?(m, include_private = false)
113         @#{obj}.respond_to?(m, include_private)
114       end
115     }
116   end
117 end
118
119 class Object
120   def ancestors
121     ret = []
122     klass = self.class
123
124     until klass == Object
125       ret << klass
126       klass = klass.superclass
127     end
128     ret
129   end
130
131   ## "k combinator"
132   def returning x; yield x; x; end
133
134   ## clone of java-style whole-method synchronization
135   ## assumes a @mutex variable
136   ## TODO: clean up, try harder to avoid namespace collisions
137   def synchronized *methods
138     methods.each do |meth|
139       class_eval <<-EOF
140         alias unsynchronized_#{meth} #{meth}
141         def #{meth}(*a, &b)
142           @mutex.synchronize { unsynchronized_#{meth}(*a, &b) }
143         end
144       EOF
145     end
146   end
147
148   def ignore_concurrent_calls *methods
149     methods.each do |meth|
150       mutex = "@__concurrent_protector_#{meth}"
151       flag = "@__concurrent_flag_#{meth}"
152       oldmeth = "__unprotected_#{meth}"
153       class_eval <<-EOF
154         alias #{oldmeth} #{meth}
155         def #{meth}(*a, &b)
156           #{mutex} = Mutex.new unless defined? #{mutex}
157           #{flag} = true unless defined? #{flag}
158           run = #{mutex}.synchronize do
159             if #{flag}
160               #{flag} = false
161               true
162             end
163           end
164           if run
165             ret = #{oldmeth}(*a, &b)
166             #{mutex}.synchronize { #{flag} = true }
167             ret
168           end
169         end
170       EOF
171     end
172   end
173 end
174
175 class String
176   ## nasty multibyte hack for ruby 1.8. if it's utf-8, split into chars using
177   ## the utf8 regex and count those. otherwise, use the byte length.
178   def display_length
179     if $encoding == "UTF-8"
180       scan(/./u).size
181     else
182       size
183     end
184   end
185
186   def camel_to_hyphy
187     self.gsub(/([a-z])([A-Z0-9])/, '\1-\2').downcase
188   end
189
190   def find_all_positions x
191     ret = []
192     start = 0
193     while start < length
194       pos = index x, start
195       break if pos.nil?
196       ret << pos
197       start = pos + 1
198     end
199     ret
200   end
201
202   ## a very complicated regex found on teh internets to split on
203   ## commas, unless they occurr within double quotes.
204   def split_on_commas
205     split(/,\s*(?=(?:[^"]*"[^"]*")*(?![^"]*"))/)
206   end
207
208   ## ok, here we do it the hard way. got to have a remainder for purposes of
209   ## tab-completing full email addresses
210   def split_on_commas_with_remainder
211     ret = []
212     state = :outstring
213     pos = 0
214     region_start = 0
215     while pos <= length
216       newpos = case state
217         when :escaped_instring, :escaped_outstring then pos
218         else index(/[,"\\]/, pos)
219       end
220
221       if newpos
222         char = self[newpos]
223       else
224         char = nil
225         newpos = length
226       end
227
228       case char
229       when ?"
230         state = case state
231           when :outstring then :instring
232           when :instring then :outstring
233           when :escaped_instring then :instring
234           when :escaped_outstring then :outstring
235         end
236       when ?,, nil
237         state = case state
238           when :outstring, :escaped_outstring then
239             ret << self[region_start ... newpos].gsub(/^\s+|\s+$/, "")
240             region_start = newpos + 1
241             :outstring
242           when :instring then :instring
243           when :escaped_instring then :instring
244         end
245       when ?\\
246         state = case state
247           when :instring then :escaped_instring
248           when :outstring then :escaped_outstring
249           when :escaped_instring then :instring
250           when :escaped_outstring then :outstring
251         end
252       end
253       pos = newpos + 1
254     end
255
256     remainder = case state
257       when :instring
258         self[region_start .. -1].gsub(/^\s+/, "")
259       else
260         nil
261       end
262
263     [ret, remainder]
264   end
265
266   def wrap len
267     ret = []
268     s = self
269     while s.length > len
270       cut = s[0 ... len].rindex(/\s/)
271       if cut
272         ret << s[0 ... cut]
273         s = s[(cut + 1) .. -1]
274       else
275         ret << s[0 ... len]
276         s = s[len .. -1]
277       end
278     end
279     ret << s
280   end
281
282   def normalize_whitespace
283     gsub(/\t/, "    ").gsub(/\r/, "")
284   end
285
286   unless method_defined? :ord
287     def ord
288       self[0]
289     end
290   end
291
292   ## takes a list of words, and returns an array of symbols.  typically used in
293   ## Sup for translating Ferret's representation of a list of labels (a string)
294   ## to an array of label symbols.
295   ##
296   ## split_on will be passed to String#split, so you can leave this nil for space.
297   def to_set_of_symbols split_on=nil; Set.new split(split_on).map { |x| x.strip.intern } end
298 end
299
300 class Numeric
301   def clamp min, max
302     if self < min
303       min
304     elsif self > max
305       max
306     else
307       self
308     end
309   end
310
311   def in? range; range.member? self; end
312
313   def to_human_size
314     if self < 1024
315       to_s + "b"
316     elsif self < (1024 * 1024)
317       (self / 1024).to_s + "k"
318     elsif self < (1024 * 1024 * 1024)
319       (self / 1024 / 1024).to_s + "m"
320     else
321       (self / 1024 / 1024 / 1024).to_s + "g"
322     end
323   end
324 end
325
326 class Fixnum
327   def to_character
328     if self < 128 && self >= 0
329       chr
330     else
331       "<#{self}>"
332     end
333   end
334
335   ## hacking the english language
336   def pluralize s
337     to_s + " " +
338       if self == 1
339         s
340       else
341         if s =~ /(.*)y$/
342           $1 + "ies"
343         else
344           s + "s"
345         end
346       end
347   end
348 end
349
350 class Hash
351   def - o
352     Hash[*self.map { |k, v| [k, v] unless o.include? k }.compact.flatten_one_level]
353   end
354
355   def select_by_value v=true
356     select { |k, vv| vv == v }.map { |x| x.first }
357   end
358 end
359
360 module Enumerable
361   def map_with_index
362     ret = []
363     each_with_index { |x, i| ret << yield(x, i) }
364     ret
365   end
366
367   def sum; inject(0) { |x, y| x + y }; end
368   
369   def map_to_hash
370     ret = {}
371     each { |x| ret[x] = yield(x) }
372     ret
373   end
374
375   # like find, except returns the value of the block rather than the
376   # element itself.
377   def argfind
378     ret = nil
379     find { |e| ret ||= yield(e) }
380     ret || nil # force
381   end
382
383   def argmin
384     best, bestval = nil, nil
385     each do |e|
386       val = yield e
387       if bestval.nil? || val < bestval
388         best, bestval = e, val
389       end
390     end
391     best
392   end
393
394   ## returns the maximum shared prefix of an array of strings
395   ## optinally excluding a prefix
396   def shared_prefix caseless=false, exclude=""
397     return "" if empty?
398     prefix = ""
399     (0 ... first.length).each do |i|
400       c = (caseless ? first.downcase : first)[i]
401       break unless all? { |s| (caseless ? s.downcase : s)[i] == c }
402       next if exclude[i] == c
403       prefix += first[i].chr
404     end
405     prefix
406   end
407
408   def max_of
409     map { |e| yield e }.max
410   end
411 end
412
413 class Array
414   def flatten_one_level
415     inject([]) { |a, e| a + e }
416   end
417
418   def to_h; Hash[*flatten]; end
419   def rest; self[1..-1]; end
420
421   def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
422
423   def last= e; self[-1] = e end
424   def nonempty?; !empty? end
425 end
426
427 class Time
428   def to_indexable_s
429     sprintf "%012d", self
430   end
431
432   def nearest_hour
433     if min < 30
434       self
435     else
436       self + (60 - min) * 60
437     end
438   end
439
440   def midnight # within a second
441     self - (hour * 60 * 60) - (min * 60) - sec
442   end
443
444   def is_the_same_day? other
445     (midnight - other.midnight).abs < 1
446   end
447
448   def is_the_day_before? other
449     other.midnight - midnight <=  24 * 60 * 60 + 1
450   end
451
452   def to_nice_distance_s from=Time.now
453     later_than = (self < from)
454     diff = (self.to_i - from.to_i).abs.to_f
455     text = 
456       [ ["second", 60],
457         ["minute", 60],
458         ["hour", 24],
459         ["day", 7],
460         ["week", 4.345], # heh heh
461         ["month", 12],
462         ["year", nil],
463       ].argfind do |unit, size|
464         if diff.round <= 1
465           "one #{unit}"
466         elsif size.nil? || diff.round < size
467           "#{diff.round} #{unit}s"
468         else
469           diff /= size.to_f
470           false
471         end
472       end
473     if later_than
474       text + " ago"
475     else
476       "in " + text
477     end  
478   end
479
480   TO_NICE_S_MAX_LEN = 9 # e.g. "Yest.10am"
481   def to_nice_s from=Time.now
482     if year != from.year
483       strftime "%b %Y"
484     elsif month != from.month
485       strftime "%b %e"
486     else
487       if is_the_same_day? from
488         strftime("%l:%M%P")
489       elsif is_the_day_before? from
490         "Yest."  + nearest_hour.strftime("%l%P")
491       else
492         strftime "%b %e"
493       end
494     end
495   end
496 end
497
498 ## simple singleton module. far less complete and insane than the ruby
499 ## standard library one, but automatically forwards methods calls and
500 ## allows for constructors that take arguments.
501 ##
502 ## You must have #initialize call "self.class.i_am_the_instance self"
503 ## at some point or everything will fail horribly.
504 module Singleton
505   module ClassMethods
506     def instance; @instance; end
507     def instantiated?; defined?(@instance) && !@instance.nil?; end
508     def deinstantiate!; @instance = nil; end
509     def method_missing meth, *a, &b
510       raise "no instance defined!" unless defined? @instance
511
512       ## if we've been deinstantiated, just drop all calls. this is
513       ## useful because threads that might be active during the
514       ## cleanup process (e.g. polling) would otherwise have to
515       ## special-case every call to a Singleton object
516       return nil if @instance.nil?
517
518       @instance.send meth, *a, &b
519     end
520     def i_am_the_instance o
521       raise "there can be only one! (instance)" if defined? @instance
522       @instance = o
523     end
524   end
525
526   def self.included klass
527     klass.extend ClassMethods
528   end
529 end
530
531 ## wraps an object. if it throws an exception, keeps a copy.
532 class Recoverable
533   def initialize o
534     @o = o
535     @error = nil
536     @mutex = Mutex.new
537   end
538
539   attr_accessor :error
540
541   def clear_error!; @error = nil; end
542   def has_errors?; !@error.nil?; end
543
544   def method_missing m, *a, &b; __pass m, *a, &b end
545
546   def id; __pass :id; end
547   def to_s; __pass :to_s; end
548   def to_yaml x; __pass :to_yaml, x; end
549   def is_a? c; @o.is_a? c; end
550
551   def respond_to?(m, include_private=false)
552     @o.respond_to?(m, include_private)
553   end
554
555   def __pass m, *a, &b
556     begin
557       @o.send(m, *a, &b)
558     rescue Exception => e
559       @error ||= e
560       raise
561     end
562   end
563 end
564
565 ## acts like a hash with an initialization block, but saves any
566 ## newly-created value even upon lookup.
567 ##
568 ## for example:
569 ##
570 ## class C
571 ##   attr_accessor :val
572 ##   def initialize; @val = 0 end
573 ## end
574 ## 
575 ## h = Hash.new { C.new }
576 ## h[:a].val # => 0
577 ## h[:a].val = 1
578 ## h[:a].val # => 0
579 ##
580 ## h2 = SavingHash.new { C.new }
581 ## h2[:a].val # => 0
582 ## h2[:a].val = 1
583 ## h2[:a].val # => 1
584 ##
585 ## important note: you REALLY want to use #member? to test existence,
586 ## because just checking h[anything] will always evaluate to true
587 ## (except for degenerate constructor blocks that return nil or false)
588 class SavingHash
589   def initialize &b
590     @constructor = b
591     @hash = Hash.new
592   end
593
594   def [] k
595     @hash[k] ||= @constructor.call(k)
596   end
597
598   defer_all_other_method_calls_to :hash
599 end
600
601 class OrderedHash < Hash
602   alias_method :store, :[]=
603   alias_method :each_pair, :each
604   attr_reader :keys
605
606   def initialize *a
607     @keys = []
608     a.each { |k, v| self[k] = v }
609   end
610
611   def []= key, val
612     @keys << key unless member?(key)
613     super
614   end
615
616   def values; keys.map { |k| self[k] } end
617   def index key; @keys.index key end
618
619   def delete key
620     @keys.delete key
621     super
622   end
623
624   def each; @keys.each { |k| yield k, self[k] } end
625 end
626
627 ## easy thread-safe class for determining who's the "winner" in a race (i.e.
628 ## first person to hit the finish line
629 class FinishLine
630   def initialize
631     @m = Mutex.new
632     @over = false
633   end
634
635   def winner?
636     @m.synchronize { !@over && @over = true }
637   end
638 end
639
640 class Iconv
641   def self.easy_decode target, charset, text
642     return text if charset =~ /^(x-unknown|unknown[-_ ]?8bit|ascii[-_ ]?7[-_ ]?bit)$/i
643     charset = case charset
644       when /UTF[-_ ]?8/i then "utf-8"
645       when /(iso[-_ ])?latin[-_ ]?1$/i then "ISO-8859-1"
646       when /iso[-_ ]?8859[-_ ]?15/i then 'ISO-8859-15'
647       when /unicode[-_ ]1[-_ ]1[-_ ]utf[-_]7/i then "utf-7"
648       else charset
649     end
650
651     begin
652       Iconv.iconv(target + "//IGNORE", charset, text + " ").join[0 .. -2]
653     rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::InvalidCharacter, Iconv::IllegalSequence => e
654       Redwood::log "warning: error (#{e.class.name}) decoding text from #{charset} to #{target}: #{text[0 ... 20]}"
655       text
656     end
657   end
658 end