예제 #1
0
class DaemonMonitor(xbmc.Monitor):
    _settings_prefix = "s"
    _settings_separator = ":"
    _settings_get_uri = "settings/get"
    _settings_set_uri = "settings/set/?reset=true"

    settings_name = "settings.json"
    log_name = "torrest.log"

    def __init__(self):
        super(DaemonMonitor, self).__init__()
        self._lock = threading.Lock()
        self._daemon = Daemon(
            "torrest",
            os.path.join(kodi.ADDON_PATH, "resources", "bin",
                         get_platform_arch()),
            work_dir=kodi.ADDON_DATA,
            android_extra_dirs=(xbmc.translatePath("special://xbmcbin"), ),
            dest_dir=os.path.join(kodi.ADDON_DATA, "bin"),
            pid_file=os.path.join(kodi.ADDON_DATA, ".pid"),
            root=run_as_root())
        self._daemon.ensure_exec_permissions()
        self._daemon.kill_leftover_process()
        self._port = self._enabled = None
        self._settings_path = os.path.join(kodi.ADDON_DATA, self.settings_name)
        self._log_path = os.path.join(kodi.ADDON_DATA, self.log_name)
        self._settings_spec = [
            s for s in kodi.get_all_settings_spec()
            if s["id"].startswith(self._settings_prefix +
                                  self._settings_separator)
        ]

    def _start(self):
        self._daemon.start("-port",
                           str(self._port),
                           "-settings",
                           self._settings_path,
                           level=logging.INFO,
                           path=self._log_path)

    def _stop(self):
        self._daemon.stop()

    def _request(self, method, url, **kwargs):
        return requests.request(
            method, "http://127.0.0.1:{}/{}".format(self._port, url), **kwargs)

    def _wait(self, timeout=-1, notification=False):
        start = time.time()
        while not 0 < timeout < time.time() - start:
            try:
                self._request("get", "")
                if notification:
                    kodi.notification(kodi.translate(30104))
                return
            except requests.exceptions.ConnectionError:
                if self.waitForAbort(0.5):
                    raise AbortRequestedError("Abort requested")
        raise DaemonTimeoutError("Timeout reached")

    def _get_kodi_settings(self):
        s = kodi.generate_dict_settings(
            self._settings_spec,
            separator=self._settings_separator)[self._settings_prefix]
        s["download_path"] = assure_unicode(
            xbmc.translatePath(s["download_path"]))
        s["torrents_path"] = assure_unicode(
            xbmc.translatePath(s["torrents_path"]))
        return s

    def _get_daemon_settings(self):
        r = self._request("get", self._settings_get_uri)
        if r.status_code != 200:
            logging.error("Failed getting daemon settings with code %d: %s",
                          r.status_code, r.text)
            return None
        return r.json()

    def _update_kodi_settings(self):
        daemon_settings = self._get_daemon_settings()
        if daemon_settings is None:
            return False
        kodi.set_settings_dict(daemon_settings,
                               prefix=self._settings_prefix,
                               separator=self._settings_separator)
        return True

    def _update_daemon_settings(self):
        daemon_settings = self._get_daemon_settings()
        if daemon_settings is None:
            return False

        kodi_settings = self._get_kodi_settings()
        if daemon_settings != kodi_settings:
            logging.debug("Need to update daemon settings")
            r = self._request("post",
                              self._settings_set_uri,
                              json=kodi_settings)
            if r.status_code != 200:
                xbmcgui.Dialog().ok(kodi.translate(30102), r.json()["error"])
                return False

        return True

    def onSettingsChanged(self):
        with self._lock:
            port_changed = enabled_changed = False

            port = get_port()
            if port != self._port:
                self._port = port
                port_changed = True

            enabled = service_enabled()
            if enabled != self._enabled:
                self._enabled = enabled
                enabled_changed = True

            if self._enabled:
                if port_changed and not enabled_changed:
                    self._stop()
                if port_changed or enabled_changed:
                    self._start()
                    self._wait(timeout=get_daemon_timeout(), notification=True)
                self._update_daemon_settings()
            elif enabled_changed:
                self._stop()

    def handle_crashes(self, max_crashes=5, max_consecutive_crash_time=20):
        crash_count = 0
        last_crash = 0

        while not self.waitForAbort(1):
            # Initial check to avoid using the lock most of the time
            if self._daemon.daemon_poll() is None:
                continue

            with self._lock:
                if self._enabled and self._daemon.daemon_poll() is not None:
                    logging.warning("Deamon crashed")
                    kodi.notification(kodi.translate(30105))
                    self._stop()

                    if os.path.exists(self._log_path):
                        path = os.path.join(
                            kodi.ADDON_DATA,
                            time.strftime("%Y%m%d_%H%M%S.") + self.log_name)
                        shutil.copy(self._log_path, path)

                    crash_time = time.time()
                    time_between_crashes = crash_time - last_crash
                    if 0 < max_consecutive_crash_time < time_between_crashes:
                        crash_count = 1
                    else:
                        crash_count += 1

                    if last_crash > 0:
                        logging.info("%.2f seconds passed since last crash",
                                     time_between_crashes)
                    last_crash = crash_time

                    if crash_count <= max_crashes:
                        logging.info("Re-starting daemon - %s/%s", crash_count,
                                     max_crashes)

                        if crash_count > 1 and os.path.exists(
                                self._settings_path):
                            logging.info("Removing old settings file")
                            os.remove(self._settings_path)

                        self._start()

                        try:
                            self._wait(timeout=get_daemon_timeout(),
                                       notification=True)
                            self._update_daemon_settings()
                        except DaemonTimeoutError:
                            logging.error("Timed out waiting for daemon")
                            last_crash = time.time()
                    else:
                        logging.info("Max crashes (%d) reached", max_crashes)

    def __enter__(self):
        try:
            self.onSettingsChanged()
        except DaemonTimeoutError:
            logging.error("Timed out waiting for daemon")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._stop()
        return exc_type is AbortRequestedError