#!/usr/bin/env python3
# -*- coding:utf-8 -*-
#
#   Author  :   renyuneyun
#   E-mail  :   renyuneyun@gmail.com
#   Date    :   2017-04-03 18:27:34
#   License :   GPLv2+
#
import os
import re
import argparse
import sys

def processes_with_deleted_files():
    pids = [int(pid) for pid in os.listdir('/proc') if pid.isdigit()]
    pattern = re.compile('/dev|/run|/drm |/SYSV.*$|/memfd|/\[aio\]')
    targets = set() # [(pid, filename), ...]
    for pid in pids:
        try:
            with open(os.path.join('/proc', str(pid), 'maps'), 'r') as fmap:
                for line in fmap:
                    parts = [part.strip() for part in line.split(' ', 5)]
                    fname = parts[-1]
                    if fname.rfind('(deleted)') != -1:
                        fname = fname.rstrip('(deleted)').strip()
                        if not pattern.match(fname):
                            #targets.append((pid, fname))
                            targets.add((pid, fname))
        except FileNotFoundError: # proc has already terminated
            continue
        except PermissionError: # not enough permission
            continue
    return sorted(list(targets))

def process_name(pid):
    def stat_name(pid):
        with open(os.path.join('/proc', str(pid), 'stat'), 'r') as stat:
            return stat.readline().split()[1].strip()[1:-1]
    def exe_name(pid):
        return os.path.realpath(os.readlink("/proc/{}/exe".format(pid)))
    def prune_path(program):
        dirname, filename = os.path.split(program)
        if dirname in os.environ['PATH'].split(os.pathsep):
            return filename
        return program
    def process_cmdline(pid):
        return open("/proc/{}/cmdline".format(pid), 'rb').read()[:-1].split(b'\0')
    program = stat_name(pid)
    if len(program) < 15: #not a possibly truncated one
        return program
    program = prune_path(exe_name(pid))
    if re.match(r'python[\d+(.\d)?]?', program): #python script. The second entry in cmdline is the real program (because 'python' is less than 15 chars so a bare `python` run won't interfere)
        cmdline = process_cmdline(pid)
        if len(cmdline) == 1: #Shouldn't match (see the comment one line above). Just in case
            program = prune_path(cmdline[0].decode())
        else:
            program = prune_path(cmdline[1].decode())
    return program

def _create_process_file_map(func=str):
    results = []
    for (pid, fname) in processes_with_deleted_files():
        try:
            results.append((func(pid), fname))
        except FileNotFoundError:
            continue
        except PermissionError:
            continue
    return results

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Show running processes that use deleted files')
    parser.add_argument('-q', help='do not display title', action="store_true")
    parser.add_argument('-i', help='do not resolve process name', action="store_true")
    args = parser.parse_args()
    def lx(x):
        return lambda lst: len(lst[x])
    def limit_len_left(s, max_len):
        if max_len == -1:
            return s
        return "{:<{}s}".format(s[:max_len], max_len)
    if not args.i:
        results = _create_process_file_map(process_name)
    else:
        results = _create_process_file_map(str)
    if len(results) == 0:
        sys.exit()
    if not args.q:
        triples = [("PROCESS", "FILENAME")] + results
    MAXLEN_PNAME = max(map(lx(0), triples))
    MAXLEN_FNAME = max(map(lx(1), triples))
    for pname, fname in triples:
        print("{}\t{}".format(limit_len_left(pname, MAXLEN_PNAME), limit_len_left(fname, MAXLEN_FNAME)))

