]> git.cworth.org Git - sup/blob - lib/sup/util.rb
rewrite logging to have multiple levels: debug, info, etc.
[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 standard
499 ## library one, but it automatically forwards methods calls and allows for
500 ## constructors that take arguments.
501 ##
502 ## classes that inherit this can define initialize. however, you cannot call
503 ## .new on the class. To get the instance of the class, call .instance;
504 ## to create the instance, call init.
505 module Singleton
506   module ClassMethods
507     def instance; @instance; end
508     def instantiated?; defined?(@instance) && !@instance.nil?; end
509     def deinstantiate!; @instance = nil; end
510     def method_missing meth, *a, &b
511       raise "no #{name} instance defined in method call to #{meth}!" unless defined? @instance
512
513       ## if we've been deinstantiated, just drop all calls. this is
514       ## useful because threads that might be active during the
515       ## cleanup process (e.g. polling) would otherwise have to
516       ## special-case every call to a Singleton object
517       return nil if @instance.nil?
518
519       @instance.send meth, *a, &b
520     end
521     def init *args
522       raise "there can be only one! (instance)" if defined? @instance
523       @instance = new(*args)
524     end
525   end
526
527   def self.included klass
528     klass.private_class_method :allocate, :new
529     klass.extend ClassMethods
530   end
531 end
532
533 ## wraps an object. if it throws an exception, keeps a copy.
534 class Recoverable
535   def initialize o
536     @o = o
537     @error = nil
538     @mutex = Mutex.new
539   end
540
541   attr_accessor :error
542
543   def clear_error!; @error = nil; end
544   def has_errors?; !@error.nil?; end
545
546   def method_missing m, *a, &b; __pass m, *a, &b end
547
548   def id; __pass :id; end
549   def to_s; __pass :to_s; end
550   def to_yaml x; __pass :to_yaml, x; end
551   def is_a? c; @o.is_a? c; end
552
553   def respond_to?(m, include_private=false)
554     @o.respond_to?(m, include_private)
555   end
556
557   def __pass m, *a, &b
558     begin
559       @o.send(m, *a, &b)
560     rescue Exception => e
561       @error ||= e
562       raise
563     end
564   end
565 end
566
567 ## acts like a hash with an initialization block, but saves any
568 ## newly-created value even upon lookup.
569 ##
570 ## for example:
571 ##
572 ## class C
573 ##   attr_accessor :val
574 ##   def initialize; @val = 0 end
575 ## end
576 ## 
577 ## h = Hash.new { C.new }
578 ## h[:a].val # => 0
579 ## h[:a].val = 1
580 ## h[:a].val # => 0
581 ##
582 ## h2 = SavingHash.new { C.new }
583 ## h2[:a].val # => 0
584 ## h2[:a].val = 1
585 ## h2[:a].val # => 1
586 ##
587 ## important note: you REALLY want to use #member? to test existence,
588 ## because just checking h[anything] will always evaluate to true
589 ## (except for degenerate constructor blocks that return nil or false)
590 class SavingHash
591   def initialize &b
592     @constructor = b
593     @hash = Hash.new
594   end
595
596   def [] k
597     @hash[k] ||= @constructor.call(k)
598   end
599
600   defer_all_other_method_calls_to :hash
601 end
602
603 class OrderedHash < Hash
604   alias_method :store, :[]=
605   alias_method :each_pair, :each
606   attr_reader :keys
607
608   def initialize *a
609     @keys = []
610     a.each { |k, v| self[k] = v }
611   end
612
613   def []= key, val
614     @keys << key unless member?(key)
615     super
616   end
617
618   def values; keys.map { |k| self[k] } end
619   def index key; @keys.index key end
620
621   def delete key
622     @keys.delete key
623     super
624   end
625
626   def each; @keys.each { |k| yield k, self[k] } end
627 end
628
629 ## easy thread-safe class for determining who's the "winner" in a race (i.e.
630 ## first person to hit the finish line
631 class FinishLine
632   def initialize
633     @m = Mutex.new
634     @over = false
635   end
636
637   def winner?
638     @m.synchronize { !@over && @over = true }
639   end
640 end
641
642 class Iconv
643   def self.easy_decode target, charset, text
644     return text if charset =~ /^(x-unknown|unknown[-_ ]?8bit|ascii[-_ ]?7[-_ ]?bit)$/i
645     charset = case charset
646       when /UTF[-_ ]?8/i then "utf-8"
647       when /(iso[-_ ])?latin[-_ ]?1$/i then "ISO-8859-1"
648       when /iso[-_ ]?8859[-_ ]?15/i then 'ISO-8859-15'
649       when /unicode[-_ ]1[-_ ]1[-_ ]utf[-_]7/i then "utf-7"
650       else charset
651     end
652
653     begin
654       Iconv.iconv(target + "//IGNORE", charset, text + " ").join[0 .. -2]
655     rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::InvalidCharacter, Iconv::IllegalSequence => e
656       info "couldn't transcode text from #{charset} to #{target} (\"#{text[0 ... 20]}\"...)"
657       text
658     end
659   end
660 end