{
  "version": "1.8.2",
  "script": "import subprocess, threading\n\n# The ManagedProcess class encapsulates the run and cancel functions and streams logs internally.\nclass ManagedProcess:\n    @staticmethod\n    def _stream_logs(stream, logger, prefix):\n        # iter() reads until \"\" or EOF\n        for line in iter(stream.readline, \"\"):\n            logger(f\"{prefix}{line.rstrip()}\")\n        stream.close()\n\n    @staticmethod\n    def run(js7Step, cmd):\n        if not cmd:\n            raise ValueError(\"No command specified: 'cmd' must be a non-empty list\")\n\n        process = subprocess.Popen(\n            cmd,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            text=True,\n            bufsize=1\n        )\n\n        js7Step.setCancelableResource(process)\n\n        # Defines the thread for live stdout\n        stdout_thread = threading.Thread(\n            target=ManagedProcess._stream_logs,\n            args=(process.stdout, js7Step.getLogger().info, \"[LIVE STDOUT] \"),\n            daemon=False\n        )\n\n        # Defines the thread for live stderr\n        stderr_thread = threading.Thread(\n            target=ManagedProcess._stream_logs,\n            args=(process.stderr, js7Step.getLogger().error, \"[LIVE STDERR] \"),\n            daemon=False\n        )\n        \n        # Starts threads to read stdout and stderr streams\n        stdout_thread.start()\n        stderr_thread.start()      \n\n        # Waits until the process is finish\n        rc = process.wait()\n        \n        # Ensures threads have finished reading\n        stdout_thread.join()\n        stderr_thread.join()\n\n        return rc\n\n    @staticmethod\n    def cancel(js7Step):\n        if js7Step is None:\n            return\n        \n        # Gets the cancelable resource\n        process = js7Step.getCancelableResource()\n        if process is None:\n            return\n        \n        # Kills the subprocess if it is still running\n        try:\n            if process.poll() is None:\n                js7Step.getLogger().info(f\"[ManagedProcess.cancel][kill]{process}...\")\n                process.kill()\n        except Exception as e:\n            js7Step.getLogger().error(f\"[ManagedProcess.cancel][{process}]{e}\")\n\n        return process.wait()"
}