# 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

"""Catch various exceptions given system call"""

import errno, signal, exceptions
import librsync, C, static, rpath, Globals, log, statistics, connection

def check_common_error(error_handler, function, args = []):
	"""Apply function to args, if error, run error_handler on exception

	This uses the catch_error predicate below to only catch
	certain exceptions which seems innocent enough.

	"""
	try: return function(*args)
	except Exception, exc:
		TracebackArchive.add([function] + list(args))
		if catch_error(exc):
			log.Log.exception()
			conn = Globals.backup_writer
			if conn is not None: conn.statistics.record_error()
			if error_handler: return error_handler(exc, *args)
			else: return None
		if is_routine_fatal(exc): log.Log.exception(1, 6)
		else: log.Log.exception(1, 2)
		raise

def catch_error(exc):
	"""Return true if exception exc should be caught"""
	for exception_class in (rpath.SkipFileException, rpath.RPathException,
							librsync.librsyncError, C.UnknownFileTypeError):
		if isinstance(exc, exception_class): return 1
	if (isinstance(exc, EnvironmentError) and
		# the invalid mode shows up in backups of /proc for some reason
		(exc[0] in ('invalid mode: rb', 'Not a gzipped file') or
		 errno.errorcode.has_key(exc[0]) and
		 errno.errorcode[exc[0]] in ('EPERM', 'ENOENT', 'EACCES', 'EBUSY',
									 'EEXIST', 'ENOTDIR',
									 'ENAMETOOLONG', 'EINTR',
									 'ENOTEMPTY', 'EIO', 'ETXTBSY',
									 'ESRCH', 'EINVAL', 'EDEADLOCK',
									 'EDEADLK'))):
		return 1
	return 0

def is_routine_fatal(exc):
	"""Return string if exception is non-error unrecoverable, None otherwise

	Used to suppress a stack trace for exceptions like keyboard
	interrupts or connection drops.  Return value is string to use as
	an exit message.

	"""
	if isinstance(exc, exceptions.KeyboardInterrupt):
		return "User abort"
	elif isinstance(exc, connection.ConnectionError):
		return "Lost connection to the remote system"
	elif isinstance(exc, SignalException):
		return "Killed with signal %s" % (exc,)
	return None

def get_error_handler(error_type):
	"""Return error handler function that can be used above

	Function will just log error to the error_log and then return
	None.  First two arguments must be the exception and then an rp
	(from which the filename will be extracted).

	"""
	def error_handler(exc, rp, *args):
		log.ErrorLog.write_if_open(error_type, rp, exc)
		return 0
	return error_handler

def listrp(rp):
	"""Like rp.listdir() but return [] if error, and sort results"""
	def error_handler(exc):
		log.Log("Error listing directory %s" % rp.path, 2)
		return []
	dir_listing = check_common_error(error_handler, rp.listdir)
	dir_listing.sort()
	return dir_listing

def signal_handler(signum, frame):
	"""This is called when signal signum is caught"""
	raise SignalException(signum)

def install_signal_handlers():
	"""Install signal handlers on current connection"""
	for signum in [signal.SIGQUIT, signal.SIGHUP, signal.SIGTERM]:
		signal.signal(signum, signal_handler)


class SignalException(Exception):
	"""SignalException(signum) means signal signum has been received"""
	pass


class TracebackArchive:
	"""Save last 10 caught exceptions, so they can be printed if fatal"""
	_traceback_strings = []
	def add(cls, extra_args = []):
		"""Add most recent exception to archived list

		If extra_args are present, convert to strings and add them as
		extra information to same traceback archive.

		"""
		cls._traceback_strings.append(log.Log.exception_to_string(extra_args))
		if len(cls._traceback_strings) > 10:
			cls._traceback_strings = cls._traceback_strings[:10]

	def log(cls):
		"""Print all exception information to log file"""
		if cls._traceback_strings:
			log.Log("------------ Old traceback info -----------\n%s\n"
					"-------------------------------------------" %
					("\n".join(cls._traceback_strings),), 3)

static.MakeClass(TracebackArchive)

