]> git.cworth.org Git - sup/blob - lib/sup/util.rb
Merge branch 'master' into next
[sup] / lib / sup / util.rb
1 require 'thread'
2 require 'lockfile'
3 require 'mime/types'
4 require 'pathname'
5
6 ## time for some monkeypatching!
7 class Lockfile
8   def gen_lock_id
9     Hash[
10          'host' => "#{ Socket.gethostname }",
11          'pid' => "#{ Process.pid }",
12          'ppid' => "#{ Process.ppid }",
13          'time' => timestamp,
14          'pname' => $0,
15          'user' => ENV["USER"]
16         ]
17   end
18
19   def dump_lock_id lock_id = @lock_id
20       "host: %s\npid: %s\nppid: %s\ntime: %s\nuser: %s\npname: %s\n" %
21         lock_id.values_at('host','pid','ppid','time','user', 'pname')
22     end
23
24   def lockinfo_on_disk
25     h = load_lock_id IO.read(path)
26     h['mtime'] = File.mtime path
27     h['path'] = 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 space-separated list of words, and returns an array of symbols.
293   ## typically used in Sup for translating Ferret's representation of a list
294   ## of labels (a string) to an array of label symbols.
295   def symbolistize; split.map { |x| x.intern } end
296 end
297
298 class Numeric
299   def clamp min, max
300     if self < min
301       min
302     elsif self > max
303       max
304     else
305       self
306     end
307   end
308
309   def in? range; range.member? self; end
310
311   def to_human_size
312     if self < 1024
313       to_s + "b"
314     elsif self < (1024 * 1024)
315       (self / 1024).to_s + "k"
316     elsif self < (1024 * 1024 * 1024)
317       (self / 1024 / 1024).to_s + "m"
318     else
319       (self / 1024 / 1024 / 1024).to_s + "g"
320     end
321   end
322 end
323
324 class Fixnum
325   def to_character
326     if self < 128 && self >= 0
327       chr
328     else
329       "<#{self}>"
330     end
331   end
332
333   ## hacking the english language
334   def pluralize s
335     to_s + " " +
336       if self == 1
337         s
338       else
339         if s =~ /(.*)y$/
340           $1 + "ies"
341         else
342           s + "s"
343         end
344       end
345   end
346 end
347
348 class Hash
349   def - o
350     Hash[*self.map { |k, v| [k, v] unless o.include? k }.compact.flatten_one_level]
351   end
352
353   def select_by_value v=true
354     select { |k, vv| vv == v }.map { |x| x.first }
355   end
356 end
357
358 module Enumerable
359   def map_with_index
360     ret = []
361     each_with_index { |x, i| ret << yield(x, i) }
362     ret
363   end
364
365   def sum; inject(0) { |x, y| x + y }; end
366   
367   def map_to_hash
368     ret = {}
369     each { |x| ret[x] = yield(x) }
370     ret
371   end
372
373   # like find, except returns the value of the block rather than the
374   # element itself.
375   def argfind
376     ret = nil
377     find { |e| ret ||= yield(e) }
378     ret || nil # force
379   end
380
381   def argmin
382     best, bestval = nil, nil
383     each do |e|
384       val = yield e
385       if bestval.nil? || val < bestval
386         best, bestval = e, val
387       end
388     end
389     best
390   end
391
392   ## returns the maximum shared prefix of an array of strings
393   ## optinally excluding a prefix
394   def shared_prefix caseless=false, exclude=""
395     return "" if empty?
396     prefix = ""
397     (0 ... first.length).each do |i|
398       c = (caseless ? first.downcase : first)[i]
399       break unless all? { |s| (caseless ? s.downcase : s)[i] == c }
400       next if exclude[i] == c
401       prefix += first[i].chr
402     end
403     prefix
404   end
405
406   def max_of
407     map { |e| yield e }.max
408   end
409 end
410
411 class Array
412   def flatten_one_level
413     inject([]) { |a, e| a + e }
414   end
415
416   def to_h; Hash[*flatten]; end
417   def rest; self[1..-1]; end
418
419   def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
420
421   def last= e; self[-1] = e end
422   def nonempty?; !empty? end
423
424   def to_set_of_symbols
425     map { |x| x.is_a?(Symbol) ? x : x.intern }.uniq
426   end
427 end
428
429 class Time
430   def to_indexable_s
431     sprintf "%012d", self
432   end
433
434   def nearest_hour
435     if min < 30
436       self
437     else
438       self + (60 - min) * 60
439     end
440   end
441
442   def midnight # within a second
443     self - (hour * 60 * 60) - (min * 60) - sec
444   end
445
446   def is_the_same_day? other
447     (midnight - other.midnight).abs < 1
448   end
449
450   def is_the_day_before? other
451     other.midnight - midnight <=  24 * 60 * 60 + 1
452   end
453
454   def to_nice_distance_s from=Time.now
455     later_than = (self < from)
456     diff = (self.to_i - from.to_i).abs.to_f
457     text = 
458       [ ["second", 60],
459         ["minute", 60],
460         ["hour", 24],
461         ["day", 7],
462         ["week", 4.345], # heh heh
463         ["month", 12],
464         ["year", nil],
465       ].argfind do |unit, size|
466         if diff.round <= 1
467           "one #{unit}"
468         elsif size.nil? || diff.round < size
469           "#{diff.round} #{unit}s"
470         else
471           diff /= size.to_f
472           false
473         end
474       end
475     if later_than
476       text + " ago"
477     else
478       "in " + text
479     end  
480   end
481
482   TO_NICE_S_MAX_LEN = 9 # e.g. "Yest.10am"
483   def to_nice_s from=Time.now
484     if year != from.year
485       strftime "%b %Y"
486     elsif month != from.month
487       strftime "%b %e"
488     else
489       if is_the_same_day? from
490         strftime("%l:%M%P")
491       elsif is_the_day_before? from
492         "Yest."  + nearest_hour.strftime("%l%P")
493       else
494         strftime "%b %e"
495       end
496     end
497   end
498 end
499
500 ## simple singleton module. far less complete and insane than the ruby
501 ## standard library one, but automatically forwards methods calls and
502 ## allows for constructors that take arguments.
503 ##
504 ## You must have #initialize call "self.class.i_am_the_instance self"
505 ## at some point or everything will fail horribly.
506 module Singleton
507   module ClassMethods
508     def instance; @instance; end
509     def instantiated?; defined?(@instance) && !@instance.nil?; end
510     def deinstantiate!; @instance = nil; end
511     def method_missing meth, *a, &b
512       raise "no instance defined!" unless defined? @instance
513
514       ## if we've been deinstantiated, just drop all calls. this is
515       ## useful because threads that might be active during the
516       ## cleanup process (e.g. polling) would otherwise have to
517       ## special-case every call to a Singleton object
518       return nil if @instance.nil?
519
520       @instance.send meth, *a, &b
521     end
522     def i_am_the_instance o
523       raise "there can be only one! (instance)" if defined? @instance
524       @instance = o
525     end
526   end
527
528   def self.included klass
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       Redwood::log "warning: error (#{e.class.name}) decoding text from #{charset} to #{target}: #{text[0 ... 20]}"
657       text
658     end
659   end
660 end