]> git.cworth.org Git - sup/blob - lib/sup/util.rb
clean up thread-index-mode to synchronize better
[sup] / lib / sup / util.rb
1 require 'lockfile'
2 require 'mime/types'
3 require 'pathname'
4
5 ## time for some monkeypatching!
6 class Lockfile
7   def gen_lock_id
8     Hash[
9          'host' => "#{ Socket.gethostname }",
10          'pid' => "#{ Process.pid }",
11          'ppid' => "#{ Process.ppid }",
12          'time' => timestamp,
13          'pname' => $0,
14          'user' => ENV["USER"]
15         ]
16   end
17
18   def dump_lock_id lock_id = @lock_id
19       "host: %s\npid: %s\nppid: %s\ntime: %s\nuser: %s\npname: %s\n" %
20         lock_id.values_at('host','pid','ppid','time','user', 'pname')
21     end
22
23   def lockinfo_on_disk
24     h = load_lock_id IO.read(path)
25     h['mtime'] = File.mtime path
26     h
27   end
28
29   def touch_yourself; touch path end
30 end
31
32 class Pathname
33   def human_size
34     s =
35       begin
36         size
37       rescue SystemCallError
38         return "?"
39       end
40
41     if s < 1024
42       s.to_s + "b"
43     elsif s < (1024 * 1024)
44       (s / 1024).to_s + "k"
45     elsif s < (1024 * 1024 * 1024)
46       (s / 1024 / 1024).to_s + "m"
47     else
48       (s / 1024 / 1024 / 1024).to_s + "g"
49     end
50   end
51
52   def human_time
53     begin
54       ctime.strftime("%Y-%m-%d %H:%M")
55     rescue SystemCallError
56       "?"
57     end
58   end
59 end
60
61 ## more monkeypatching!
62 module RMail
63   class EncodingUnsupportedError < StandardError; end
64
65   class Message
66     def add_file_attachment fn
67       bfn = File.basename fn
68       a = Message.new
69       t = MIME::Types.type_for(bfn).first || MIME::Types.type_for("exe").first
70
71       a.header.add "Content-Disposition", "attachment; filename=#{bfn.to_s.inspect}"
72       a.header.add "Content-Type", "#{t.content_type}; name=#{bfn.to_s.inspect}"
73       a.header.add "Content-Transfer-Encoding", t.encoding
74       a.body =
75         case t.encoding
76         when "base64"
77           [IO.read(fn)].pack "m"
78         when "quoted-printable"
79           [IO.read(fn)].pack "M"
80         when "7bit", "8bit"
81           IO.read(fn)
82         else
83           raise EncodingUnsupportedError, t.encoding
84         end
85
86       add_part a
87     end
88
89     def charset
90       if header.field?("content-type") && header.fetch("content-type") =~ /charset="?(.*?)"?(;|$)/
91         $1
92       end
93     end
94   end
95 end
96
97 class Range
98   ## only valid for integer ranges (unless I guess it's exclusive)
99   def size 
100     last - first + (exclude_end? ? 0 : 1)
101   end
102 end
103
104 class Module
105   def bool_reader *args
106     args.each { |sym| class_eval %{ def #{sym}?; @#{sym}; end } }
107   end
108   def bool_writer *args; attr_writer(*args); end
109   def bool_accessor *args
110     bool_reader(*args)
111     bool_writer(*args)
112   end
113
114   def defer_all_other_method_calls_to obj
115     class_eval %{
116       def method_missing meth, *a, &b; @#{obj}.send meth, *a, &b; end
117       def respond_to? meth; @#{obj}.respond_to?(meth); end
118     }
119   end
120 end
121
122 class Object
123   def ancestors
124     ret = []
125     klass = self.class
126
127     until klass == Object
128       ret << klass
129       klass = klass.superclass
130     end
131     ret
132   end
133
134   ## "k combinator"
135   def returning x; yield x; x; end
136
137   ## clone of java-style whole-method synchronization
138   ## assumes a @mutex variable
139   ## TODO: clean up, try harder to avoid namespace collisions
140   def synchronized *meth
141     meth.each do
142       class_eval <<-EOF
143         alias unsynchronized_#{meth} #{meth}
144         def #{meth}(*a, &b)
145           @mutex.synchronize { unsynchronized_#{meth}(*a, &b) }
146         end
147       EOF
148     end
149   end
150
151   def ignore_concurrent_calls *meth
152     meth.each do
153       mutex = "@__concurrent_protector_#{meth}"
154       flag = "@__concurrent_flag_#{meth}"
155       oldmeth = "__unprotected_#{meth}"
156       class_eval <<-EOF
157         alias #{oldmeth} #{meth}
158         def #{meth}(*a, &b)
159           #{mutex} = Mutex.new unless defined? #{mutex}
160           #{flag} = true unless defined? #{flag}
161           run = #{mutex}.synchronize do
162             if #{flag}
163               #{flag} = false
164               true
165             end
166           end
167           if run
168             ret = #{oldmeth}(*a, &b)
169             #{mutex}.synchronize { #{flag} = true }
170             ret
171           end
172         end
173       EOF
174     end
175   end
176 end
177
178 class String
179   def camel_to_hyphy
180     self.gsub(/([a-z])([A-Z0-9])/, '\1-\2').downcase
181   end
182
183   def find_all_positions x
184     ret = []
185     start = 0
186     while start < length
187       pos = index x, start
188       break if pos.nil?
189       ret << pos
190       start = pos + 1
191     end
192     ret
193   end
194
195   def ucfirst
196     self[0 .. 0].upcase + self[1 .. -1]
197   end
198
199   ## a very complicated regex found on teh internets to split on
200   ## commas, unless they occurr within double quotes.
201   def split_on_commas
202     split(/,\s*(?=(?:[^"]*"[^"]*")*(?![^"]*"))/)
203   end
204
205   def wrap len
206     ret = []
207     s = self
208     while s.length > len
209       cut = s[0 ... len].rindex(/\s/)
210       if cut
211         ret << s[0 ... cut]
212         s = s[(cut + 1) .. -1]
213       else
214         ret << s[0 ... len]
215         s = s[len .. -1]
216       end
217     end
218     ret << s
219   end
220
221   def normalize_whitespace
222     gsub(/\t/, "    ").gsub(/\r/, "")
223   end
224 end
225
226 class Numeric
227   def clamp min, max
228     if self < min
229       min
230     elsif self > max
231       max
232     else
233       self
234     end
235   end
236
237   def in? range; range.member? self; end
238 end
239
240 class Fixnum
241   def num_digits base=10
242     return 1 if self == 0
243     1 + (Math.log(self) / Math.log(10)).floor
244   end
245   
246   def to_character
247     if self < 128 && self >= 0
248       chr
249     else
250       "<#{self}>"
251     end
252   end
253 end
254
255 class Hash
256   def - o
257     Hash[*self.map { |k, v| [k, v] unless o.include? k }.compact.flatten_one_level]
258   end
259
260   def select_by_value v=true
261     select { |k, vv| vv == v }.map { |x| x.first }
262   end
263 end
264
265 module Enumerable
266   def map_with_index
267     ret = []
268     each_with_index { |x, i| ret << yield(x, i) }
269     ret
270   end
271
272   def sum; inject(0) { |x, y| x + y }; end
273   
274   def map_to_hash
275     ret = {}
276     each { |x| ret[x] = yield(x) }
277     ret
278   end
279
280   # like find, except returns the value of the block rather than the
281   # element itself.
282   def argfind
283     ret = nil
284     find { |e| ret ||= yield(e) }
285     ret || nil # force
286   end
287
288   def argmin
289     best, bestval = nil, nil
290     each do |e|
291       val = yield e
292       if bestval.nil? || val < bestval
293         best, bestval = e, val
294       end
295     end
296     best
297   end
298
299   ## returns the maximum shared prefix of an array of strings
300   ## optinally excluding a prefix
301   def shared_prefix caseless=false, exclude=""
302     return "" if empty?
303     prefix = ""
304     (0 ... first.length).each do |i|
305       c = (caseless ? first.downcase : first)[i]
306       break unless all? { |s| (caseless ? s.downcase : s)[i] == c }
307       next if exclude[i] == c
308       prefix += first[i].chr
309     end
310     prefix
311   end
312
313   def max_of
314     map { |e| yield e }.max
315   end
316 end
317
318 class Array
319   def flatten_one_level
320     inject([]) { |a, e| a + e }
321   end
322
323   def to_h; Hash[*flatten]; end
324   def rest; self[1..-1]; end
325
326   def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
327
328   def last= e; self[-1] = e end
329 end
330
331 class Time
332   def to_indexable_s
333     sprintf "%012d", self
334   end
335
336   def nearest_hour
337     if min < 30
338       self
339     else
340       self + (60 - min) * 60
341     end
342   end
343
344   def midnight # within a second
345     self - (hour * 60 * 60) - (min * 60) - sec
346   end
347
348   def is_the_same_day? other
349     (midnight - other.midnight).abs < 1
350   end
351
352   def is_the_day_before? other
353     other.midnight - midnight <=  24 * 60 * 60 + 1
354   end
355
356   def to_nice_distance_s from=Time.now
357     later_than = (self < from)
358     diff = (self.to_i - from.to_i).abs.to_f
359     text = 
360       [ ["second", 60],
361         ["minute", 60],
362         ["hour", 24],
363         ["day", 7],
364         ["week", 4.345], # heh heh
365         ["month", 12],
366         ["year", nil],
367       ].argfind do |unit, size|
368         if diff.round <= 1
369           "one #{unit}"
370         elsif size.nil? || diff.round < size
371           "#{diff.round} #{unit}s"
372         else
373           diff /= size.to_f
374           false
375         end
376       end
377     if later_than
378       text + " ago"
379     else
380       "in " + text
381     end  
382   end
383
384   TO_NICE_S_MAX_LEN = 9 # e.g. "Yest.10am"
385   def to_nice_s from=Time.now
386     if year != from.year
387       strftime "%b %Y"
388     elsif month != from.month
389       strftime "%b %e"
390     else
391       if is_the_same_day? from
392         strftime("%l:%M%P")
393       elsif is_the_day_before? from
394         "Yest."  + nearest_hour.strftime("%l%P")
395       else
396         strftime "%b %e"
397       end
398     end
399   end
400 end
401
402 ## simple singleton module. far less complete and insane than the ruby
403 ## standard library one, but automatically forwards methods calls and
404 ## allows for constructors that take arguments.
405 ##
406 ## You must have #initialize call "self.class.i_am_the_instance self"
407 ## at some point or everything will fail horribly.
408 module Singleton
409   module ClassMethods
410     def instance; @instance; end
411     def instantiated?; defined?(@instance) && !@instance.nil?; end
412     def deinstantiate!; @instance = nil; end
413     def method_missing meth, *a, &b
414       raise "no instance defined!" unless defined? @instance
415
416       ## if we've been deinstantiated, just drop all calls. this is
417       ## useful because threads that might be active during the
418       ## cleanup process (e.g. polling) would otherwise have to
419       ## special-case every call to a Singleton object
420       return nil if @instance.nil?
421
422       @instance.send meth, *a, &b
423     end
424     def i_am_the_instance o
425       raise "there can be only one! (instance)" if defined? @instance
426       @instance = o
427     end
428   end
429
430   def self.included klass
431     klass.extend ClassMethods
432   end
433 end
434
435 ## wraps an object. if it throws an exception, keeps a copy, and
436 ## rethrows it for any further method calls.
437 class Recoverable
438   def initialize o
439     @o = o
440     @e = nil
441   end
442
443   def clear_error!; @e = nil; end
444   def has_errors?; !@e.nil?; end
445   def error; @e; end
446
447   def method_missing m, *a, &b; __pass m, *a, &b; end
448   
449   def id; __pass :id; end
450   def to_s; __pass :to_s; end
451   def to_yaml x; __pass :to_yaml, x; end
452   def is_a? c; @o.is_a? c; end
453
454   def respond_to? m; @o.respond_to? m end
455
456   def __pass m, *a, &b
457     begin
458       @o.send(m, *a, &b)
459     rescue Exception => e
460       @e = e
461       raise e
462     end
463   end
464 end
465
466 ## acts like a hash with an initialization block, but saves any
467 ## newly-created value even upon lookup.
468 ##
469 ## for example:
470 ##
471 ## class C
472 ##   attr_accessor :val
473 ##   def initialize; @val = 0 end
474 ## end
475 ## 
476 ## h = Hash.new { C.new }
477 ## h[:a].val # => 0
478 ## h[:a].val = 1
479 ## h[:a].val # => 0
480 ##
481 ## h2 = SavingHash.new { C.new }
482 ## h2[:a].val # => 0
483 ## h2[:a].val = 1
484 ## h2[:a].val # => 1
485 ##
486 ## important note: you REALLY want to use #member? to test existence,
487 ## because just checking h[anything] will always evaluate to true
488 ## (except for degenerate constructor blocks that return nil or false)
489 class SavingHash
490   def initialize &b
491     @constructor = b
492     @hash = Hash.new
493   end
494
495   def [] k
496     @hash[k] ||= @constructor.call(k)
497   end
498
499   defer_all_other_method_calls_to :hash
500 end