]> git.cworth.org Git - sup/blob - lib/sup.rb
multiple concurrent process detection and resolution
[sup] / lib / sup.rb
1 require 'rubygems'
2 require 'yaml'
3 require 'zlib'
4 require 'thread'
5 require 'fileutils'
6 require 'lockfile'
7
8 ## time for some monkeypatching!
9 class Lockfile
10   def gen_lock_id
11     Hash[
12          'host' => "#{ Socket.gethostname }",
13          'pid' => "#{ Process.pid }",
14          'ppid' => "#{ Process.ppid }",
15          'time' => timestamp,
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\n" %
22         lock_id.values_at('host','pid','ppid','time','user')
23     end
24
25   def lockinfo_on_disk
26     load_lock_id IO.read(path)
27   end
28 end
29
30 class Object
31   ## this is for debugging purposes because i keep calling #id on the
32   ## wrong object and i want it to throw an exception
33   def id
34     raise "wrong id called on #{self.inspect}"
35   end
36 end
37
38 class LockError < StandardError
39   def initialize h
40     super ""
41     @h = h
42   end
43
44   def method_missing m; @h[m.to_s] end
45 end
46
47 class Module
48   def yaml_properties *props
49     props = props.map { |p| p.to_s }
50     vars = props.map { |p| "@#{p}" }
51     klass = self
52     path = klass.name.gsub(/::/, "/")
53     
54     klass.instance_eval do
55       define_method(:to_yaml_properties) { vars }
56       define_method(:to_yaml_type) { "!#{Redwood::YAML_DOMAIN},#{Redwood::YAML_DATE}/#{path}" }
57     end
58
59     YAML.add_domain_type("#{Redwood::YAML_DOMAIN},#{Redwood::YAML_DATE}", path) do |type, val|
60       klass.new(*props.map { |p| val[p] })
61     end
62   end
63 end
64
65 module Redwood
66   VERSION = "0.0.8"
67
68   BASE_DIR   = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
69   CONFIG_FN  = File.join(BASE_DIR, "config.yaml")
70   SOURCE_FN  = File.join(BASE_DIR, "sources.yaml")
71   LABEL_FN   = File.join(BASE_DIR, "labels.txt")
72   PERSON_FN  = File.join(BASE_DIR, "people.txt")
73   CONTACT_FN = File.join(BASE_DIR, "contacts.txt")
74   DRAFT_DIR  = File.join(BASE_DIR, "drafts")
75   SENT_FN    = File.join(BASE_DIR, "sent.mbox")
76   LOCK_FN    = File.join(BASE_DIR, "lock")
77   SUICIDE_FN = File.join(BASE_DIR, "please-kill-yourself")
78
79   YAML_DOMAIN = "masanjin.net"
80   YAML_DATE = "2006-10-01"
81
82 ## record exceptions thrown in threads nicely
83   $exception = nil
84   def reporting_thread
85     if $opts[:no_threads]
86       yield
87     else
88       ::Thread.new do
89         begin
90           yield
91         rescue Exception => e
92           File.open("sup-exception-log.txt", "w") do |f|
93             f.puts "--- #{e.class.name} at #{Time.now}"
94             f.puts e.message, e.backtrace
95           end unless e.is_a? SuicideException
96           $exception ||= e
97           raise
98         end
99       end
100     end
101   end
102   module_function :reporting_thread
103
104 ## one-stop shop for yamliciousness
105   def save_yaml_obj object, fn, compress=false
106     if compress
107       Zlib::GzipWriter.open(fn) { |f| f.puts object.to_yaml }
108     else
109       File.open(fn, "w") { |f| f.puts object.to_yaml }
110     end
111   end
112
113   def load_yaml_obj fn, compress=false
114     if File.exists? fn
115       if compress
116         Zlib::GzipReader.open(fn) { |f| YAML::load f }
117       else
118         YAML::load_file fn
119       end
120     end
121   end
122
123   def start
124     Redwood::PersonManager.new Redwood::PERSON_FN
125     Redwood::SentManager.new Redwood::SENT_FN
126     Redwood::ContactManager.new Redwood::CONTACT_FN
127     Redwood::LabelManager.new Redwood::LABEL_FN
128     Redwood::AccountManager.new $config[:accounts]
129     Redwood::DraftManager.new Redwood::DRAFT_DIR
130     Redwood::UpdateManager.new
131     Redwood::PollManager.new
132     Redwood::SuicideManager.new Redwood::SUICIDE_FN
133   end
134
135   def finish
136     Redwood::LabelManager.save
137     Redwood::ContactManager.save
138     Redwood::PersonManager.save
139     Redwood::BufferManager.deinstantiate!
140   end
141
142   def lock
143     FileUtils.rm_f SUICIDE_FN
144
145     Redwood::log "locking #{LOCK_FN}..."
146     $lock = Lockfile.new LOCK_FN, :retries => 0
147       begin
148         $lock.lock
149       rescue Lockfile::MaxTriesLockError
150         raise LockError, $lock.lockinfo_on_disk
151       end
152   end
153
154   def unlock
155     Redwood::log "unlocking #{LOCK_FN}..."
156     $lock.unlock if $lock
157   end
158
159   ## not really a good place for this, so I'll just dump it here.
160   def report_broken_sources opts={}
161     return unless BufferManager.instantiated?
162
163     broken_sources = Index.usual_sources.select { |s| s.error.is_a? FatalSourceError }
164     unless broken_sources.empty?
165       BufferManager.spawn "Broken source notification", TextMode.new(<<EOM), opts
166 Source error notification
167 -------------------------
168
169 Hi there. It looks like one or more message sources is reporting
170 errors. Until this is corrected, messages from these sources cannot
171 be viewed, and new messages will not be detected.
172
173 #{broken_sources.map { |s| "Source: " + s.to_s + "\n Error: " + s.error.message.wrap(70).join("\n        ")}.join("\n\n")}
174 EOM
175 #' stupid ruby-mode
176     end
177
178     desynced_sources = Index.usual_sources.select { |s| s.error.is_a? OutOfSyncSourceError }
179     unless desynced_sources.empty?
180       BufferManager.spawn "Out-of-sync source notification", TextMode.new(<<EOM), opts
181 Out-of-sync source notification
182 -------------------------------
183
184 Hi there. It looks like one or more sources has fallen out of sync
185 with my index. This can happen when you modify these sources with
186 other email clients. (Sorry, I don't play well with others.)
187
188 Until this is corrected, messages from these sources cannot be viewed,
189 and new messages will not be detected. Luckily, this is easy to correct!
190
191 #{desynced_sources.map do |s|
192   "Source: " + s.to_s + 
193    "\n Error: " + s.error.message.wrap(70).join("\n        ") + 
194    "\n   Fix: sup-sync --changed #{s.to_s}"
195   end}
196 EOM
197 #' stupid ruby-mode
198     end
199   end
200
201   module_function :save_yaml_obj, :load_yaml_obj, :start, :finish,
202                   :lock, :unlock, :report_broken_sources
203 end
204
205 ## set up default configuration file
206 if File.exists? Redwood::CONFIG_FN
207   $config = Redwood::load_yaml_obj Redwood::CONFIG_FN
208 else
209   require 'etc'
210   require 'socket'
211   name = Etc.getpwnam(ENV["USER"]).gecos.split(/,/).first
212   email = ENV["USER"] + "@" + 
213     begin
214       Socket.gethostbyname(Socket.gethostname).first
215     rescue SocketError
216       Socket.gethostname
217     end
218
219   $config = {
220     :accounts => {
221       :default => {
222         :name => name,
223         :email => email,
224         :alternates => [],
225         :sendmail => "/usr/sbin/sendmail -oem -ti",
226         :signature => File.join(ENV["HOME"], ".signature")
227       }
228     },
229     :editor => ENV["EDITOR"] || "/usr/bin/vim -f -c 'setlocal spell spelllang=en_us' -c 'set filetype=mail'",
230     :thread_by_subject => false,
231   }
232   begin
233     FileUtils.mkdir_p Redwood::BASE_DIR
234     Redwood::save_yaml_obj $config, Redwood::CONFIG_FN
235   rescue StandardError => e
236     $stderr.puts "warning: #{e.message}"
237   end
238 end
239
240 require "sup/util"
241 require "sup/update"
242 require "sup/suicide"
243 require "sup/message"
244 require "sup/source"
245 require "sup/mbox"
246 require "sup/maildir"
247 require "sup/imap"
248 require "sup/person"
249 require "sup/account"
250 require "sup/thread"
251 require "sup/index"
252 require "sup/textfield"
253 require "sup/buffer"
254 require "sup/keymap"
255 require "sup/mode"
256 require "sup/colormap"
257 require "sup/label"
258 require "sup/contact"
259 require "sup/tagger"
260 require "sup/draft"
261 require "sup/poll"
262 require "sup/modes/scroll-mode"
263 require "sup/modes/text-mode"
264 require "sup/modes/line-cursor-mode"
265 require "sup/modes/help-mode"
266 require "sup/modes/edit-message-mode"
267 require "sup/modes/compose-mode"
268 require "sup/modes/resume-mode"
269 require "sup/modes/forward-mode"
270 require "sup/modes/reply-mode"
271 require "sup/modes/label-list-mode"
272 require "sup/modes/contact-list-mode"
273 require "sup/modes/thread-view-mode"
274 require "sup/modes/thread-index-mode"
275 require "sup/modes/label-search-results-mode"
276 require "sup/modes/search-results-mode"
277 require "sup/modes/person-search-results-mode"
278 require "sup/modes/inbox-mode"
279 require "sup/modes/buffer-list-mode"
280 require "sup/modes/log-mode"
281 require "sup/modes/poll-mode"
282 require "sup/logger"
283 require "sup/sent"
284
285 module Redwood
286   def log s; Logger.log s; end
287   module_function :log
288 end
289
290 $:.each do |base|
291   d = File.join base, "sup/share/modes/"
292   Redwood::Mode.load_all_modes d if File.directory? d
293 end