#! /usr/bin/python3
# SPDX-License-Identifier: LGPL-2.1-or-later

import json
import os
import pwd
import signal
import subprocess
import sys
import time

# mount-users   --  Find and terminate processes that keep a mount busy


def fuser(mount_point):

    import dbus
    bus = dbus.SystemBus()
    systemd_manager = dbus.Interface(bus.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1'),
                                     dbus_interface='org.freedesktop.systemd1.Manager')

    results = {}

    def get_cmdline(pid):
        with open(f"/proc/{pid}/cmdline") as f:
            return " ".join(f.read().split("\0"))

    def get_stat(pid):
        with open(f"/proc/{pid}/stat") as f:
            stat = f.read()
        # Field two is everything between the first "(" and the last ")", the rest is space separated.
        comm_start = stat.index("(") + 1
        comm_end = stat.rindex(")")
        return ([stat[0:comm_start - 1].strip(), stat[comm_start:comm_end],
                 *list(filter(lambda f: f != '', stat[comm_end + 1:-1].split(' ')))])

    def get_loginuser(pid):
        uid = os.stat("/proc/%s" % pid).st_uid
        try:
            return pwd.getpwuid(uid).pw_name
        except OSError:
            return uid

    def check(path, pid):
        t = os.readlink(path)
        if t == mount_point or t.startswith(mount_point + "/"):
            unit = systemd_manager.GetUnitByPID(int(pid))
            if unit not in results:
                unit_obj = bus.get_object('org.freedesktop.systemd1', unit)
                unit_id = unit_obj.Get("org.freedesktop.systemd1.Unit", "Id",
                                       dbus_interface="org.freedesktop.DBus.Properties")
                if unit_id.endswith(".scope"):
                    stat = get_stat(pid)
                    start = int(stat[21]) / os.sysconf('SC_CLK_TCK')
                    results[pid] = {"pid": int(pid),
                                    "cmd": get_cmdline(pid),
                                    "comm": stat[1],
                                    "user": get_loginuser(pid),
                                    "since": time.clock_gettime(time.CLOCK_MONOTONIC) - start}
                else:
                    desc = unit_obj.Get("org.freedesktop.systemd1.Unit", "Description",
                                        dbus_interface="org.freedesktop.DBus.Properties")
                    timestamp = unit_obj.Get("org.freedesktop.systemd1.Unit", "ActiveEnterTimestamp",
                                             dbus_interface="org.freedesktop.DBus.Properties")
                    results[unit] = {"unit": unit_id,
                                     "cmd": get_cmdline(pid),
                                     "desc": desc,
                                     "user": get_loginuser(pid),
                                     "since": time.time() - timestamp / 1e6}
            return True
        return False

    def checkdir(path, pid):
        for f in os.listdir(path):
            if check(os.path.join(path, f), pid):
                return True
        return False

    my_pid = os.getpid()

    for p in os.listdir("/proc/"):
        if not p.isdigit():
            continue
        if int(p) == my_pid:
            continue
        proc = "/proc/%s/" % p
        try:
            if check(proc + "exe", p):
                continue
            if check(proc + "root", p):
                continue
            if check(proc + "cwd", p):
                continue
            if checkdir(proc + "fd", p):
                continue
            if checkdir(proc + "map_files", p):
                continue
        except OSError:
            pass

    return list(results.values())


def users(mount_point):
    data = fuser(mount_point)
    sys.stdout.write(json.dumps(data) + "\n")
    sys.stdout.flush()


def stop_pids(pids):
    def sendsig(pid, sig):
        try:
            os.kill(pid, sig)
            return True
        except Exception:
            return False

    def sendsigs(pids, sig):
        for p in pids:
            sendsig(p, sig)

    def pollpids(pids, count, interval):
        while count > 0:
            i = 0
            while i < len(pids):
                if not sendsig(pids[i], 0):
                    pids.pop(i)
                else:
                    i += 1
            if len(pids) == 0:
                break
            count -= 1
            time.sleep(interval)

    sendsigs(pids, signal.SIGTERM)
    sendsigs(pids, signal.SIGHUP)
    pollpids(pids, 10, 0.1)
    pollpids(pids, 19, 1)
    sendsigs(pids, signal.SIGKILL)
    pollpids(pids, 10, 0.1)


def stop_units(units):
    if len(units) > 0:
        subprocess.check_call(['systemctl', 'stop', *units])


def stop(users):
    stop_pids([u["pid"] for u in users if "pid" in u])
    stop_units([u["unit"] for u in users if "unit" in u])


def dispatch(argv):
    if argv[1] == "users":
        users(argv[2])
    elif argv[1] == "stop":
        stop(json.loads(argv[2]))


try:
    dispatch(sys.argv)
except subprocess.CalledProcessError as e:
    sys.exit(e.returncode)
except Exception as e:
    sys.stderr.write(str(e) + "\n")
    sys.exit(1)
