#!/usr/bin/python3
#
# Collect information about processes which are still running after sending
# SIGTERM to them (which happens during computer shutdown in
# /etc/init.d/sendsigs in Debian/Ubuntu)
#
# Copyright (c) 2010 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 argparse
import errno
import os

import apport
import apport.fileutils
import apport.hookutils


def parse_argv():
    """Parse command line and return arguments."""
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-o",
        "--omit",
        metavar="PID",
        action="append",
        default=[],
        dest="blacklist",
        help="Ignore a particular process ID"
        " (can be specified multiple times)",
    )
    return parser.parse_args()


def orphaned_processes(blacklist):
    """Yield an iterator of running process IDs.

    This excludes PIDs which do not have a valid /proc/pid/exe symlink (e. g.
    kernel processes), the PID of our own process, and everything that is
    contained in the blacklist argument.
    """
    my_pid = os.getpid()
    my_sid = os.getsid(0)
    for d in os.listdir("/proc"):
        try:
            pid = int(d)
        except ValueError:
            continue
        if pid == 1 or pid == my_pid or d in blacklist:
            apport.warning("ignoring: %s", d)
            continue

        try:
            sid = os.getsid(pid)
        except OSError:
            # os.getsid() can fail with "No such process" if the process died
            # in the meantime
            continue

        if sid == my_sid:
            apport.warning("ignoring same sid: %s", d)
            continue

        try:
            os.readlink(os.path.join("/proc", d, "exe"))
        except OSError as error:
            if error.errno == errno.ENOENT:
                # kernel thread or similar, silently ignore
                continue
            apport.warning(
                "Could not read information about pid %s: %s", d, str(error)
            )
            continue

        yield d


def do_report(pid, blacklist):
    """Create a report for a particular PID."""

    r = apport.Report("Bug")
    try:
        r.add_proc_info(pid)
    except (ValueError, AssertionError):
        # happens if ExecutablePath doesn't exist (any more?), ignore
        return

    r["Tags"] = "shutdown-hang"
    r["Title"] = "does not terminate at computer shutdown"
    if "ExecutablePath" in r:
        r["Title"] = os.path.basename(r["ExecutablePath"]) + " " + r["Title"]
    r["Processes"] = apport.hookutils.command_output(["ps", "aux"])
    r["InitctlList"] = apport.hookutils.command_output(["initctl", "list"])
    if blacklist:
        r["OmitPids"] = " ".join(blacklist)

    try:
        with apport.fileutils.make_report_file(r) as f:
            r.write(f)
    except FileExistsError as error:
        apport.warning(
            f"Cannot create report: {error.filename} already exists"
        )
    except OSError as error:
        apport.fatal("Cannot create report: " + str(error))


#
# main
#

args = parse_argv()

for p in orphaned_processes(args.blacklist):
    do_report(p, args.blacklist)
