#!/usr/bin/python

# Collect information about a crash and create a report in the directory
# specified by apport_utils.report_dir.
# See https://wiki.ubuntu.com/AutomatedProblemReports for details.
#
# Copyright (c) 2006 Canonical Ltd.
# Author: Martin Pitt <martin.pitt@ubuntu.com>
#
# This program is free software; you can redistribute it and/or modify it
# 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.  See http://www.gnu.org/copyleft/gpl.html for
# the full text of the license.

import sys, os, os.path, subprocess, time, traceback, tempfile, glob
import signal, inspect, atexit

import problem_report, apport_utils

#################################################################
#
# functions
#
#################################################################

def cleanup(coredump):
    '''atexit handler to clean up.'''

    os.unlink(coredump)

def drop_privileges(pid):
    '''Change user and group to match the given target process.'''

    stat = None
    try:
	stat = os.stat('/proc/' + pid)
    except OSError:
	raise ValueError, 'Invalid process ID'

    os.setgid(stat.st_gid)
    os.setuid(stat.st_uid)
    assert os.getuid() == stat.st_uid

def init_error_log():
    '''Open a suitable error log if sys.stderr is not a tty.'''

    if not os.isatty(sys.stderr.fileno()):
	try:
	    f = open('/var/log/apport.log', 'a')
	except IOError: # on a permission error, don't touch stderr
	    return
	os.dup2(f.fileno(), sys.stderr.fileno())
	os.dup2(f.fileno(), sys.stdout.fileno())
	f.close()

def error_log(msg):
    '''Output something to the error log.'''

    print >> sys.stderr, 'apport (pid %s) %s:' % (os.getpid(),
	time.asctime()), msg

def _log_signal_handler(sgn, frame):
    '''Internal apport signal handler. Just log the signal handler and exit.'''

    # reset handler so that we do not get stuck in loops
    signal.signal(sgn, signal.SIG_IGN)
    try:
	error_log('Got signal %i, aborting; frame:' % sgn)
	for s in inspect.stack():
	    error_log(str(s))
    except:
	pass
    sys.exit(1)

def setup_signals():
    '''Install a signal handler for all crash-like signals, so that apport is
    not called on itself when apport crashed.'''

    signal.signal(signal.SIGILL, _log_signal_handler)
    signal.signal(signal.SIGABRT, _log_signal_handler)
    signal.signal(signal.SIGFPE, _log_signal_handler)
    signal.signal(signal.SIGSEGV, _log_signal_handler)
    signal.signal(signal.SIGPIPE, _log_signal_handler)
    signal.signal(signal.SIGBUS, _log_signal_handler)

#################################################################
#
# main
#
#################################################################

init_error_log()
try:
    setup_signals()

    if len(sys.argv) < 3 or len(sys.argv) > 4:
	print >> sys.stderr, 'Usage:', sys.argv[0], '<pid> <signal number> [<path to core dump>]'
	sys.exit(1)


    pid = sys.argv[1]
    signum = sys.argv[2]

    # drop our process priority level to not disturb userspace so much
    try:
	os.nice(10)
    except OSError:
	pass # *shrug*, we tried

    # try to find the core dump file
    if len(sys.argv) == 4:
	coredump = sys.argv[3]
    else:
	coredump = None

    # if path is relative, prepend cwd of crashed process
    if coredump and len(coredump) > 0 and coredump[0] != '/':
	coredump = os.path.join(os.readlink('/proc/' + pid + '/cwd'), coredump)
    if coredump and not os.path.exists(coredump):
	coredump = None

    # abort if we do not have a core dump to avoid useless reports
    if not coredump:
	error_log('called with: ' + str(sys.argv) + ', but no core dump available, ignoring')
	sys.exit(1)

    if coredump and os.environ.get('REMOVE_CORE', False):
	atexit.register(cleanup, coredump)

    # ignore SIGQUIT (it's usually deliberately generated by users) and SIGABRT
    # (we currently have no way of extracting abort() messages or mono's stderr
    # for stack traces).
    if signum == str(signal.SIGQUIT) or signum == str(signal.SIGABRT):
	sys.exit(0)

    error_log('called with: ' + str(sys.argv) + ', using core file ' +
	str(coredump))

    try:
	pidstat = os.stat('/proc/' + pid)
    except OSError:
	error_log('Invalid PID')
	sys.exit(1)

    info = problem_report.ProblemReport('Crash')
    info['Signal'] = signum
    if coredump:
	info['CoreDump'] = (coredump,)

    # We already need this here to figure out the ExecutableName (for scripts,
    # etc).
    apport_utils.report_add_proc_info(info, pid)

    if not info.has_key('ExecutablePath'):
	error_log('could not determine ExecutablePath, aborting')
	sys.exit(1)

    if info.has_key('InterpreterPath'):
	error_log('script: %s, interpreted by %s (command line "%s")' %
	    (info['ExecutablePath'], info['InterpreterPath'],
	    info['ProcCmdline']))
    else:
	error_log('executable: %s (command line "%s")' %
	    (info['ExecutablePath'], info['ProcCmdline']))

    # ignore non-package binaries
    if not apport_utils.find_file_package(info['ExecutablePath']):
	error_log('executable does not belong to a package, ignoring')
	sys.exit(1)

    crash_counter = 0

    # Create crash report file descriptor. We prefer to create the report in
    # report_dir if we can create a file there; if not, we just use stderr.
    try:
	report = '%s/%s.%i.crash' % (apport_utils.report_dir, info['ExecutablePath'].replace('/', '_'), pidstat.st_uid)
	if os.path.exists(report):
	    if apport_utils.seen_report(report):
		# do not flood the logs and the user with repeated crashes
		crash_counter = apport_utils.get_recent_crashes(open(report))
		crash_counter += 1
		if crash_counter > 1:
		    sys.exit(1)
	    else:
		error_log('apport: report %s already exists and unseen, doing nothing to avoid disk usage DoS' % report)
		sys.exit(1)
	reportfile = open(report, 'w')
	os.chmod(report, 0000)
	os.chown(report, pidstat.st_uid, pidstat.st_gid)
    except (OSError, IOError):
	report = None
	reportfile = sys.stderr

    drop_privileges(pid)

    if crash_counter > 0:
	info['CrashCounter'] = '%i' % crash_counter

    info.write(reportfile)
    if report:
	os.chmod(report, 0600)
    if reportfile != sys.stderr:
	error_log('wrote report %s' % report)
except (SystemExit, KeyboardInterrupt):
    raise
except Exception, e:
    error_log('Unhandled exception:')
    traceback.print_exc()
    print >> sys.stderr, 'pid: %i, uid: %i, gid: %i, euid: %i, egid: %i' % (
       os.getpid(), os.getuid(), os.getgid(), os.geteuid(), os.getegid())
