summaryrefslogtreecommitdiff
path: root/rdiff-backup/rdiff_backup/log.py
blob: 1097503a0155c60e4d7dec19b9e4f7f703e91b38 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# Copyright 2002 Ben Escoto
#
# This file is part of rdiff-backup.
#
# rdiff-backup is free software; you can redistribute it and/or modify
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# rdiff-backup is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with rdiff-backup; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA

"""Manage logging, displaying and recording messages with required verbosity"""

import time, sys, traceback, types, rpath
import Globals, static, re


class LoggerError(Exception): pass

class Logger:
	"""All functions which deal with logging"""
	def __init__(self):
		self.log_file_open = None
		self.log_file_local = None
		self.verbosity = self.term_verbosity = 3
		# termverbset is true if the term_verbosity has been explicity set
		self.termverbset = None

	def setverbosity(self, verbosity_string):
		"""Set verbosity levels.  Takes a number string"""
		try: self.verbosity = int(verbosity_string)
		except ValueError:
			Log.FatalError("Verbosity must be a number, received '%s' "
						   "instead." % verbosity_string)
		if not self.termverbset: self.term_verbosity = self.verbosity

	def setterm_verbosity(self, termverb_string):
		"""Set verbosity to terminal.  Takes a number string"""
		try: self.term_verbosity = int(termverb_string)
		except ValueError:
			Log.FatalError("Terminal verbosity must be a number, received "
						   "'%s' instead." % termverb_string)
		self.termverbset = 1

	def open_logfile(self, rpath):
		"""Inform all connections of an open logfile.

		rpath.conn will write to the file, and the others will pass
		write commands off to it.

		"""
		assert not self.log_file_open
		rpath.conn.log.Log.open_logfile_local(rpath)
		for conn in Globals.connections:
			conn.log.Log.open_logfile_allconn(rpath.conn)

	def open_logfile_allconn(self, log_file_conn):
		"""Run on all connections to signal log file is open"""
		self.log_file_open = 1
		self.log_file_conn = log_file_conn

	def open_logfile_local(self, rpath):
		"""Open logfile locally - should only be run on one connection"""
		assert rpath.conn is Globals.local_connection
		try: self.logfp = rpath.open("a")
		except (OSError, IOError), e:
			raise LoggerError("Unable to open logfile %s: %s"
							  % (rpath.path, e))
		self.log_file_local = 1
		self.logrp = rpath

	def close_logfile(self):
		"""Close logfile and inform all connections"""
		if self.log_file_open:
			for conn in Globals.connections:
				conn.log.Log.close_logfile_allconn()
			self.log_file_conn.log.Log.close_logfile_local()

	def close_logfile_allconn(self):
		"""Run on every connection"""
		self.log_file_open = None

	def close_logfile_local(self):
		"""Run by logging connection - close logfile"""
		assert self.log_file_conn is Globals.local_connection
		assert not self.logfp.close()
		self.log_file_local = None

	def format(self, message, verbosity):
		"""Format the message, possibly adding date information"""
		if verbosity < 9: return message + "\n"
		else: return "%s  %s\n" % (time.asctime(time.localtime(time.time())),
								   message)

	def __call__(self, message, verbosity):
		"""Log message that has verbosity importance

		message can be a string, which is logged as-is, or a function,
		which is then called and should return the string to be
		logged.  We do it this way in case producing the string would
		take a significant amount of CPU.
		
		"""
		if verbosity > self.verbosity and verbosity > self.term_verbosity:
			return

		if not (type(message) is types.StringType
				or type(message) is types.UnicodeType):
			assert type(message) is types.FunctionType
			message = message()

		if verbosity <= self.verbosity: self.log_to_file(message)
		if verbosity <= self.term_verbosity:
			self.log_to_term(message, verbosity)

	def log_to_file(self, message):
		"""Write the message to the log file, if possible"""
		if self.log_file_open:
			if self.log_file_local:
				str = self.format(message, self.verbosity)
				if type(str) != unicode:
					str = unicode(str, 'utf-8')
				str = str.encode('utf-8')
				self.logfp.write(str)
				self.logfp.flush()
			else: self.log_file_conn.log.Log.log_to_file(message)

	def log_to_term(self, message, verbosity):
		"""Write message to stdout/stderr"""
		if verbosity <= 2 or Globals.server: termfp = sys.stderr
		else: termfp = sys.stdout
		str = self.format(message, self.term_verbosity)
		if type(str) != unicode:
			str = unicode(str, 'utf-8')
		try:
			# Try to log as unicode, but fall back to ascii (for Windows)
			termfp.write(str.encode('utf-8'))
		except UnicodeDecodeError:
			termfp.write(str.encode('ascii', 'replace'))

	def conn(self, direction, result, req_num):
		"""Log some data on the connection

		The main worry with this function is that something in here
		will create more network traffic, which will spiral to
		infinite regress.  So, for instance, logging must only be done
		to the terminal, because otherwise the log file may be remote.

		"""
		if self.term_verbosity < 9: return
		if type(result) is types.StringType: result_repr = repr(result)
		else: result_repr = str(result)
		if Globals.server: conn_str = "Server"
		else: conn_str = "Client"
		self.log_to_term("%s %s (%d): %s" %
						 (conn_str, direction, req_num, result_repr), 9)

	def FatalError(self, message, no_fatal_message = 0, errlevel = 1):
		"""Log a fatal error and exit"""
		assert no_fatal_message == 0 or no_fatal_message == 1
		if no_fatal_message: prefix_string = ""
		else: prefix_string = "Fatal Error: "
		self.log_to_term(prefix_string + message, 1)
		#import Main
		#Main.cleanup()
		sys.exit(errlevel)

	def exception_to_string(self, arglist = []):
		"""Return string version of current exception plus what's in arglist"""
		type, value, tb = sys.exc_info()
		s = (u"Exception '%s' raised of class '%s':\n%s" %
			 (value, type, u"".join(traceback.format_tb(tb))))
		s = s.encode('ascii', 'replace')
		if arglist:
			s += "__Arguments:"
			for arg in arglist:
				s += "\n"
				try:
					s += str(arg)
				except UnicodeError:
					s += unicode(arg).encode('ascii', 'replace')
		return s

	def exception(self, only_terminal = 0, verbosity = 5):
		"""Log an exception and traceback

		If only_terminal is None, log normally.  If it is 1, then only
		log to disk if log file is local (self.log_file_open = 1).  If
		it is 2, don't log to disk at all.

		"""
		assert only_terminal in (0, 1, 2)
		if (only_terminal == 0 or
			(only_terminal == 1 and self.log_file_open)):
			logging_func = self.__call__
		else:
			logging_func = self.log_to_term
			if verbosity >= self.term_verbosity: return

		exception_string = self.exception_to_string()
		try:
			logging_func(exception_string, verbosity)
		except IOError:
			print "IOError while trying to log exception!"
			print exception_string


Log = Logger()


class ErrorLog:
	"""Log each recoverable error in error_log file

	There are three types of recoverable errors:  ListError, which
	happens trying to list a directory or stat a file, UpdateError,
	which happen when trying to update a changed file, and
	SpecialFileError, which happen when a special file cannot be
	created.  See the error policy file for more info.

	"""
	_log_fileobj = None
	def open(cls, time_string, compress = 1):
		"""Open the error log, prepare for writing"""
		if not Globals.isbackup_writer:
			return Globals.backup_writer.log.ErrorLog.open(time_string,
														   compress)
		assert not cls._log_fileobj, "log already open"
		assert Globals.isbackup_writer

		base_rp = Globals.rbdir.append("error_log.%s.data" % (time_string,))
		if compress: cls._log_fileobj = rpath.MaybeGzip(base_rp)
		else: cls._log_fileobj = base_rp.open("wb", compress = 0)

	def isopen(cls):
		"""True if the error log file is currently open"""
		if Globals.isbackup_writer or not Globals.backup_writer:
			return cls._log_fileobj is not None
		else: return Globals.backup_writer.log.ErrorLog.isopen()

	def write(cls, error_type, rp, exc):
		"""Add line to log file indicating error exc with file rp"""
		if not Globals.isbackup_writer:
			return Globals.backup_writer.log.ErrorLog.write(error_type,
															rp, exc)
		s = cls.get_log_string(error_type, rp, exc)
		Log(s, 2)
		if Globals.null_separator: s += "\0"
		else:
			s = re.sub("\n", " ", s)
			s += "\n"
		cls._log_fileobj.write(s)

	def get_indexpath(cls, obj):
		"""Return filename for logging.  rp is a rpath, string, or tuple"""
		try: return obj.get_indexpath()
		except AttributeError:
			if type(obj) is types.TupleType: return "/".join(obj)
			else: return str(obj)

	def write_if_open(cls, error_type, rp, exc):
		"""Call cls.write(...) if error log open, only log otherwise"""
		if not Globals.isbackup_writer and Globals.backup_writer:
			return Globals.backup_writer.log.ErrorLog.write_if_open(
				error_type, rp, str(exc)) # convert exc bc of exc picking prob
		if cls.isopen(): cls.write(error_type, rp, exc)
		else: Log(cls.get_log_string(error_type, rp, exc), 2)

	def get_log_string(cls, error_type, rp, exc):
		"""Return log string to put in error log"""
		assert (error_type == "ListError" or error_type == "UpdateError" or
				error_type == "SpecialFileError"), "Unknown type "+error_type
		str = u"%s %s %s" % (error_type, cls.get_indexpath(rp), unicode(exc))
		return str.encode('utf-8')

	def close(cls):
		"""Close the error log file"""
		if not Globals.isbackup_writer:
			return Globals.backup_writer.log.ErrorLog.close()
		assert not cls._log_fileobj.close()
		cls._log_fileobj = None

static.MakeClass(ErrorLog)