]> code.delx.au - offlineimap/blob - offlineimap/ui/UIBase.py
Merge branch 'master' of http://git.complete.org/offlineimap
[offlineimap] / offlineimap / ui / UIBase.py
1 # UI base class
2 # Copyright (C) 2002 John Goerzen
3 # <jgoerzen@complete.org>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
19 import offlineimap.version
20 import re, time, sys, traceback, threading, thread
21 from StringIO import StringIO
22 from Queue import Empty
23
24 debugtypes = {'imap': 'IMAP protocol debugging',
25 'maildir': 'Maildir repository debugging',
26 'thread': 'Threading debugging'}
27
28 globalui = None
29 def setglobalui(newui):
30 global globalui
31 globalui = newui
32 def getglobalui():
33 global globalui
34 return globalui
35
36 class UIBase:
37 def __init__(s, config, verbose = 0):
38 s.verbose = verbose
39 s.config = config
40 s.debuglist = []
41 s.debugmessages = {}
42 s.debugmsglen = 50
43 s.threadaccounts = {}
44 s.logfile = None
45
46 ################################################## UTILS
47 def _msg(s, msg):
48 """Generic tool called when no other works."""
49 s._log(msg)
50 s._display(msg)
51
52 def _log(s, msg):
53 """Log it to disk. Returns true if it wrote something; false
54 otherwise."""
55 if s.logfile:
56 s.logfile.write("%s: %s\n" % (threading.currentThread().getName(),
57 msg))
58 return 1
59 return 0
60
61 def setlogfd(s, logfd):
62 s.logfile = logfd
63 logfd.write("This is %s %s\n" % \
64 (offlineimap.version.productname,
65 offlineimap.version.versionstr))
66 logfd.write("Python: %s\n" % sys.version)
67 logfd.write("Platform: %s\n" % sys.platform)
68 logfd.write("Args: %s\n" % sys.argv)
69
70 def _display(s, msg):
71 """Display a message."""
72 raise NotImplementedError
73
74 def warn(s, msg, minor = 0):
75 if minor:
76 s._msg("warning: " + msg)
77 else:
78 s._msg("WARNING: " + msg)
79
80 def registerthread(s, account):
81 """Provides a hint to UIs about which account this particular
82 thread is processing."""
83 if s.threadaccounts.has_key(threading.currentThread()):
84 raise ValueError, "Thread %s already registered (old %s, new %s)" %\
85 (threading.currentThread().getName(),
86 s.getthreadaccount(s), account)
87 s.threadaccounts[threading.currentThread()] = account
88
89 def unregisterthread(s, thr):
90 """Recognizes a thread has exited."""
91 if s.threadaccounts.has_key(thr):
92 del s.threadaccounts[thr]
93
94 def getthreadaccount(s, thr = None):
95 if not thr:
96 thr = threading.currentThread()
97 if s.threadaccounts.has_key(thr):
98 return s.threadaccounts[thr]
99 return '*Control'
100
101 def debug(s, debugtype, msg):
102 thisthread = threading.currentThread()
103 if s.debugmessages.has_key(thisthread):
104 s.debugmessages[thisthread].append("%s: %s" % (debugtype, msg))
105 else:
106 s.debugmessages[thisthread] = ["%s: %s" % (debugtype, msg)]
107
108 while len(s.debugmessages[thisthread]) > s.debugmsglen:
109 s.debugmessages[thisthread] = s.debugmessages[thisthread][1:]
110
111 if debugtype in s.debuglist:
112 if not s._log("DEBUG[%s]: %s" % (debugtype, msg)):
113 s._display("DEBUG[%s]: %s" % (debugtype, msg))
114
115 def add_debug(s, debugtype):
116 global debugtypes
117 if debugtype in debugtypes:
118 if not debugtype in s.debuglist:
119 s.debuglist.append(debugtype)
120 s.debugging(debugtype)
121 else:
122 s.invaliddebug(debugtype)
123
124 def debugging(s, debugtype):
125 global debugtypes
126 s._msg("Now debugging for %s: %s" % (debugtype, debugtypes[debugtype]))
127
128 def invaliddebug(s, debugtype):
129 s.warn("Invalid debug type: %s" % debugtype)
130
131 def locked(s):
132 raise Exception, "Another OfflineIMAP is running with the same metadatadir; exiting."
133
134 def getnicename(s, object):
135 prelimname = str(object.__class__).split('.')[-1]
136 # Strip off extra stuff.
137 return re.sub('(Folder|Repository)', '', prelimname)
138
139 def isusable(s):
140 """Returns true if this UI object is usable in the current
141 environment. For instance, an X GUI would return true if it's
142 being run in X with a valid DISPLAY setting, and false otherwise."""
143 return 1
144
145 ################################################## INPUT
146
147 def getpass(s, accountname, config, errmsg = None):
148 raise NotImplementedError
149
150 def folderlist(s, list):
151 return ', '.join(["%s[%s]" % (s.getnicename(x), x.getname()) for x in list])
152
153 ################################################## WARNINGS
154 def msgtoreadonly(s, destfolder, uid, content, flags):
155 if not (s.config.has_option('general', 'ignore-readonly') and s.config.getboolean("general", "ignore-readonly")):
156 s.warn("Attempted to synchronize message %d to folder %s[%s], but that folder is read-only. The message will not be copied to that folder." % \
157 (uid, s.getnicename(destfolder), destfolder.getname()))
158
159 def flagstoreadonly(s, destfolder, uidlist, flags):
160 if not (s.config.has_option('general', 'ignore-readonly') and s.config.getboolean("general", "ignore-readonly")):
161 s.warn("Attempted to modify flags for messages %s in folder %s[%s], but that folder is read-only. No flags have been modified for that message." % \
162 (str(uidlist), s.getnicename(destfolder), destfolder.getname()))
163
164 def deletereadonly(s, destfolder, uidlist):
165 if not (s.config.has_option('general', 'ignore-readonly') and s.config.getboolean("general", "ignore-readonly")):
166 s.warn("Attempted to delete messages %s in folder %s[%s], but that folder is read-only. No messages have been deleted in that folder." % \
167 (str(uidlist), s.getnicename(destfolder), destfolder.getname()))
168
169 ################################################## MESSAGES
170
171 def init_banner(s):
172 """Called when the UI starts. Must be called before any other UI
173 call except isusable(). Displays the copyright banner. This is
174 where the UI should do its setup -- TK, for instance, would
175 create the application window here."""
176 if s.verbose >= 0:
177 s._msg(offlineimap.version.banner)
178
179 def connecting(s, hostname, port):
180 if s.verbose < 0:
181 return
182 if hostname == None:
183 hostname = ''
184 if port != None:
185 port = ":%s" % str(port)
186 displaystr = ' to %s%s.' % (hostname, port)
187 if hostname == '' and port == None:
188 displaystr = '.'
189 s._msg("Establishing connection" + displaystr)
190
191 def acct(s, accountname):
192 if s.verbose >= 0:
193 s._msg("***** Processing account %s" % accountname)
194
195 def acctdone(s, accountname):
196 if s.verbose >= 0:
197 s._msg("***** Finished processing account " + accountname)
198
199 def syncfolders(s, srcrepos, destrepos):
200 if s.verbose >= 0:
201 s._msg("Copying folder structure from %s to %s" % \
202 (s.getnicename(srcrepos), s.getnicename(destrepos)))
203
204 ############################## Folder syncing
205 def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder):
206 """Called when a folder sync operation is started."""
207 if s.verbose >= 0:
208 s._msg("Syncing %s: %s -> %s" % (srcfolder.getname(),
209 s.getnicename(srcrepos),
210 s.getnicename(destrepos)))
211
212 def skippingfolder(s, folder):
213 """Called when a folder sync operation is started."""
214 if s.verbose >= 0:
215 s._msg("Skipping %s (not changed)" % folder.getname())
216
217 def validityproblem(s, folder):
218 s.warn("UID validity problem for folder %s (repo %s) (saved %d; got %d); skipping it" % \
219 (folder.getname(), folder.getrepository().getname(),
220 folder.getsaveduidvalidity(), folder.getuidvalidity()))
221
222 def loadmessagelist(s, repos, folder):
223 if s.verbose > 0:
224 s._msg("Loading message list for %s[%s]" % (s.getnicename(repos),
225 folder.getname()))
226
227 def messagelistloaded(s, repos, folder, count):
228 if s.verbose > 0:
229 s._msg("Message list for %s[%s] loaded: %d messages" % \
230 (s.getnicename(repos), folder.getname(), count))
231
232 ############################## Message syncing
233
234 def syncingmessages(s, sr, sf, dr, df):
235 if s.verbose > 0:
236 s._msg("Syncing messages %s[%s] -> %s[%s]" % (s.getnicename(sr),
237 sf.getname(),
238 s.getnicename(dr),
239 df.getname()))
240
241 def copyingmessage(s, uid, src, destlist):
242 if s.verbose >= 0:
243 ds = s.folderlist(destlist)
244 s._msg("Copy message %d %s[%s] -> %s" % (uid, s.getnicename(src),
245 src.getname(), ds))
246
247 def deletingmessage(s, uid, destlist):
248 if s.verbose >= 0:
249 ds = s.folderlist(destlist)
250 s._msg("Deleting message %d in %s" % (uid, ds))
251
252 def deletingmessages(s, uidlist, destlist):
253 if s.verbose >= 0:
254 ds = s.folderlist(destlist)
255 s._msg("Deleting %d messages (%s) in %s" % \
256 (len(uidlist),
257 ", ".join([str(u) for u in uidlist]),
258 ds))
259
260 def addingflags(s, uidlist, flags, destlist):
261 if s.verbose >= 0:
262 ds = s.folderlist(destlist)
263 s._msg("Adding flags %s to %d messages on %s" % \
264 (", ".join(flags), len(uidlist), ds))
265
266 def deletingflags(s, uidlist, flags, destlist):
267 if s.verbose >= 0:
268 ds = s.folderlist(destlist)
269 s._msg("Deleting flags %s to %d messages on %s" % \
270 (", ".join(flags), len(uidlist), ds))
271
272 ################################################## Threads
273
274 def getThreadDebugLog(s, thread):
275 if s.debugmessages.has_key(thread):
276 message = "\nLast %d debug messages logged for %s prior to exception:\n"\
277 % (len(s.debugmessages[thread]), thread.getName())
278 message += "\n".join(s.debugmessages[thread])
279 else:
280 message = "\nNo debug messages were logged for %s." % \
281 thread.getName()
282 return message
283
284 def delThreadDebugLog(s, thread):
285 if s.debugmessages.has_key(thread):
286 del s.debugmessages[thread]
287
288 def getThreadExceptionString(s, thread):
289 message = "Thread '%s' terminated with exception:\n%s" % \
290 (thread.getName(), thread.getExitStackTrace())
291 message += "\n" + s.getThreadDebugLog(thread)
292 return message
293
294 def threadException(s, thread):
295 """Called when a thread has terminated with an exception.
296 The argument is the ExitNotifyThread that has so terminated."""
297 s._msg(s.getThreadExceptionString(thread))
298 s.delThreadDebugLog(thread)
299 s.terminate(100)
300
301 def getMainExceptionString(s):
302 sbuf = StringIO()
303 traceback.print_exc(file = sbuf)
304 return "Main program terminated with exception:\n" + \
305 sbuf.getvalue() + "\n" + \
306 s.getThreadDebugLog(threading.currentThread())
307
308 def mainException(s):
309 s._msg(s.getMainExceptionString())
310
311 def terminate(s, exitstatus = 0, errortitle = None, errormsg = None):
312 """Called to terminate the application."""
313 if errormsg <> None:
314 if errortitle <> None:
315 sys.stderr.write('ERROR: %s\n\n%s\n'%(errortitle, errormsg))
316 else:
317 sys.stderr.write('%s\n' % errormsg)
318 sys.exit(exitstatus)
319
320 def threadExited(s, thread):
321 """Called when a thread has exited normally. Many UIs will
322 just ignore this."""
323 s.delThreadDebugLog(thread)
324 s.unregisterthread(thread)
325
326 ################################################## Hooks
327
328 def callhook(s, msg):
329 if s.verbose >= 0:
330 s._msg(msg)
331
332 ################################################## Other
333
334 def sleep(s, sleepsecs, siglistener):
335 """This function does not actually output anything, but handles
336 the overall sleep, dealing with updates as necessary. It will,
337 however, call sleeping() which DOES output something.
338
339 Returns 0 if timeout expired, 1 if there is a request to cancel
340 the timer, and 2 if there is a request to abort the program."""
341
342 abortsleep = 0
343 while sleepsecs > 0 and not abortsleep:
344 try:
345 abortsleep = siglistener.get_nowait()
346 # retrieved signal while sleeping: 1 means immediately resynch, 2 means immediately die
347 except Empty:
348 # no signal
349 abortsleep = s.sleeping(1, sleepsecs)
350 sleepsecs -= 1
351 s.sleeping(0, 0) # Done sleeping.
352 return abortsleep
353
354 def sleeping(s, sleepsecs, remainingsecs):
355 """Sleep for sleepsecs, remainingsecs to go.
356 If sleepsecs is 0, indicates we're done sleeping.
357
358 Return 0 for normal sleep, or 1 to indicate a request
359 to sync immediately."""
360 s._msg("Next refresh in %d seconds" % remainingsecs)
361 if sleepsecs > 0:
362 time.sleep(sleepsecs)
363 return 0