#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
""" does_it_crash.py

    Runs a thing to see if it crashes within a set period.
"""
import os
import signal
import subprocess
import sys

import requests

sys.path.insert(1, os.path.dirname(sys.path[0]))

import mozinstall
import mozprocess
from mozharness.base.script import BaseScript


class DoesItCrash(BaseScript):
    config_options = [
        [
            [
                "--thing-url",
            ],
            {
                "action": "store",
                "dest": "thing_url",
                "type": str,
                "help": "An URL that points to a package containing the thing to run",
            },
        ],
        [
            [
                "--thing-to-run",
            ],
            {
                "action": "store",
                "dest": "thing_to_run",
                "type": str,
                "help": "The thing to run. If --thing-url is a package, this should be "
                "its location relative to the root of the package.",
            },
        ],
        [
            [
                "--thing-arg",
            ],
            {
                "action": "append",
                "dest": "thing_args",
                "type": str,
                "default": [],
                "help": "Args for the thing. May be passed multiple times",
            },
        ],
        [
            [
                "--run-for",
            ],
            {
                "action": "store",
                "dest": "run_for",
                "default": 30,
                "type": int,
                "help": "How long to run the thing for, in seconds",
            },
        ],
    ]

    def __init__(self):
        super(DoesItCrash, self).__init__(
            all_actions=[
                "download",
                "run-thing",
            ],
            default_actions=[
                "download",
                "run-thing",
            ],
            config_options=self.config_options,
        )

    def downloadFile(self, url, file_name):
        req = requests.get(url, stream=True, timeout=30)
        file_path = os.path.join(os.getcwd(), file_name)

        with open(file_path, "wb") as f:
            for chunk in req.iter_content(chunk_size=1024):
                if not chunk:
                    continue
                f.write(chunk)
                f.flush()
        return file_path

    def download(self):
        url = self.config["thing_url"]
        fn = "thing." + url.split(".")[-1]
        self.downloadFile(url=url, file_name=fn)
        if mozinstall.is_installer(fn):
            self.install_dir = mozinstall.install(fn, "thing")
        else:
            self.install_dir = ""

    def kill(self, proc):
        is_win = os.name == "nt"
        for retry in range(3):
            if is_win:
                proc.send_signal(signal.CTRL_BREAK_EVENT)

                # Manually kill all processes we spawned,
                # not sure why this is required, but without it we hang forever.
                process_name = self.config["thing_to_run"].split("/")[-1]
                subprocess.run(
                    ["taskkill", "/T", "/F", "/IM", process_name], check=True
                )
            else:
                os.killpg(proc.pid, signal.SIGKILL)
            try:
                proc.wait(5)
                self.log("process terminated")
                break
            except subprocess.TimeoutExpired:
                self.error("unable to terminate process!")

    def run_thing(self):
        self.timed_out = False

        def timeout_handler(proc):
            self.log(f"timeout detected: killing pid {proc.pid}")
            self.timed_out = True
            self.kill(proc)

        self.output = []

        def output_line_handler(proc, line):
            self.output.append(line)

        thing = os.path.abspath(
            os.path.join(self.install_dir, self.config["thing_to_run"])
        )
        # thing_args is a LockedTuple, which mozprocess doesn't like
        args = list(self.config["thing_args"])
        timeout = self.config["run_for"]

        self.log(f"Running {thing} with args {args}")
        cmd = [thing]
        cmd.extend(args)
        mozprocess.run_and_wait(
            cmd,
            timeout=timeout,
            timeout_handler=timeout_handler,
            output_line_handler=output_line_handler,
        )
        if not self.timed_out:
            # It crashed, oh no!
            self.critical(
                f"TEST-UNEXPECTED-FAIL: {thing} did not run for {timeout} seconds"
            )
            self.critical("Output was:")
            for l in self.output:
                self.critical(l)
            self.fatal("fail")
        else:
            self.info(f"PASS: {thing} ran successfully for {timeout} seconds")


# __main__ {{{1
if __name__ == "__main__":
    crashit = DoesItCrash()
    crashit.run_and_exit()
