#!/usr/bin/python

# Collect information about a crash and create a report in the directory
# specified by apport.fileutils.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, grp

import apport, apport.fileutils

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

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

    if os.path.exists(coredump):
	os.unlink(coredump)

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

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

    if partial:
        effective_gid = os.getegid()
        effective_uid = os.geteuid()
    else:
        effective_gid = stat.st_gid
        effective_uid = stat.st_uid

    os.setregid(stat.st_gid, effective_gid)
    os.setreuid(stat.st_uid, effective_uid)
    assert os.getegid() == effective_gid
    assert os.getgid() == stat.st_gid
    assert os.geteuid() == effective_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()):
        log = os.environ.get('APPORT_LOG_FILE', '/var/log/apport.log')
        mask = os.umask(0077)
        try:
            f = open(log, 'a')
            os.chmod(log, 0640)
            try:
                admgid = grp.getgrnam('adm')[2]
                os.chown(log, -1, admgid)
            except KeyError:
                pass # if group adm doesn't exist, just leave it as root
        except IOError: # on a permission error, don't touch stderr
            os.umask(mask)
            return
        os.umask(mask)
        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)

def write_user_coredump(pid, cwd, limit):
    '''Write the core into the current directory if ulimit requests it.'''

    # three cases:
    # limit == 0: do not write anything
    # limit == None: unlimited or large enough, write out everything
    # limit nonzero: crashed process' core size ulimit in bytes

    if limit == '0':
        return
    if limit:
        limit = int(limit)
    else:
        assert limit is None

    # limit -1 means 'unlimited'
    if limit < 0:
        limit = None
    else:
        # ulimit specifies kB
        limit *= 1024

    core_path = os.path.join(cwd, 'core')
    try:
        if open('/proc/sys/kernel/core_uses_pid').read().strip() != '0':
            core_path += '.' + str(pid)
        core_file = os.open(core_path, os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0600)
    except (OSError, IOError):
        return

    error_log('writing core dump to %s (limit: %s)' % (core_path, str(limit)))

    written = 0
    while True:
        block = sys.stdin.read(1048576)
        size = len(block)
        if size == 0:
            break
        written += size
        if limit and written > limit:
            error_log('aborting core dump writing, size exceeds current limit %i' % limit)
            os.close(core_file)
            os.unlink(core_path)
            return
        if os.write(core_file, block) != size:
            error_log('aborting core dump writing, could not write')
            os.close(core_file)
            os.unlink(core_path)
            return

    os.close(core_file)

def usable_ram():
    '''Return how many bytes of RAM is currently available that can be
    allocated without causing major trashing.'''

    # abuse our excellent RFC822 parser to parse /proc/meminfo
    r = apport.Report()
    r.load(open('/proc/meminfo'))

    memfree = long(r['MemFree'].split()[0])
    cached = long(r['Cached'].split()[0])
    writeback = long(r['Writeback'].split()[0])
    
    return (memfree+cached-writeback) * 1024


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

init_error_log()
try:
    setup_signals()

    if len(sys.argv) not in (1, 3, 4):
        print >> sys.stderr, 'Usage:'
	print >> sys.stderr, sys.argv[0], '<pid> <signal number> [<path to core dump>]'
	print >> sys.stderr, sys.argv[0], '(reading environment variables CORE_PID, CORE_SIGNAL, and reading core dump from stdin)'
        sys.exit(1)

    if len(sys.argv) >= 3:
	pid = sys.argv[1]
	signum = sys.argv[2]
	if len(sys.argv) == 4:
	    coredump = sys.argv[3]
	else:
	    coredump = None
    else:
	pid = os.environ['CORE_PID']
	signum = os.environ['CORE_SIGNAL']
	coredump = '-'

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

    # Partially drop privs to gain proper os.access() checks
    drop_privileges(pid, True)

    # try to find the core dump file; if path is relative, prepend cwd of
    # crashed process
    cwd = os.readlink('/proc/' + pid + '/cwd')
    if coredump and len(coredump) > 0 and coredump[0] not in ('/', '-'):
        coredump = os.path.join(cwd, coredump)
    if coredump and not 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 coredump != '-' and os.environ.get('REMOVE_CORE', False):
        atexit.register(cleanup, coredump)

    # ignore SIGQUIT (it's usually deliberately generated by users)
    if signum == str(signal.SIGQUIT):
        sys.exit(0)

    error_log('called for pid %s, signal %s' % (pid, signum))

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

    info = apport.Report('Crash')
    info['Signal'] = signum
    if coredump:
        if coredump == '-':
            info['CoreDump'] = (sys.stdin, True, usable_ram()*3/4)
        else:
            info['CoreDump'] = (coredump,)

    # We already need this here to figure out the ExecutableName (for scripts,
    # etc).
    info.add_proc_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.fileutils.likely_packaged(info['ExecutablePath']):
        error_log('executable does not belong to a package, ignoring')
        # check if the user wants a core dump if we use the new-style piping
        # method
        if os.environ.has_key('CORE_PID'):
            drop_privileges(pid)
            write_user_coredump(pid, cwd, os.environ.get('CORE_REAL_RLIM'))
        sys.exit(1)

    # ignore SIGABRT (we currently have no way of extracting abort() messages
    # or mono's stderr for stack traces).
    if signum == str(signal.SIGABRT):
        # check if the user wants a core dump if we use the new-style piping
        # method
        if os.environ.has_key('CORE_PID'):
            drop_privileges(pid)
            write_user_coredump(pid, cwd, os.environ.get('CORE_REAL_RLIM'))
        sys.exit(0)

    # ignore blacklisted binaries
    if info.check_ignored():
        error_log('executable version is blacklisted, 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.fileutils.report_dir, info['ExecutablePath'].replace('/', '_'), pidstat.st_uid)
        if os.path.exists(report):
            if apport.fileutils.seen_report(report):
                # do not flood the logs and the user with repeated crashes
                crash_counter = apport.fileutils.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

    # Totally drop privs before writing out the reportfile.
    drop_privileges(pid)

    # check if the user wants a core dump if we use the new-style piping
    # method
    if os.environ.has_key('CORE_PID'):
        write_user_coredump(pid, cwd, os.environ.get('CORE_REAL_RLIM'))

    info.add_user_info()
    info.add_os_info()

    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())
    print >> sys.stderr, 'environment:', os.environ
