import os import stat import time import xbmc import shutil import socket import urllib2 import xbmcgui import threading import subprocess from quasar.logger import log from quasar.osarch import PLATFORM from quasar.config import QUASARD_HOST from quasar.addon import ADDON, ADDON_ID, ADDON_PATH from quasar.util import notify, system_information, getLocalizedString, getWindowsShortPath def ensure_exec_perms(file_): st = os.stat(file_) os.chmod(file_, st.st_mode | stat.S_IEXEC) return file_ def android_get_current_appid(): with open("/proc/%d/cmdline" % os.getpid()) as fp: return fp.read().rstrip("\0") def get_quasard_checksum(path): try: with open(path) as fp: fp.seek(-40, os.SEEK_END) # we put a sha1 there return fp.read() except Exception: return "" def get_quasar_binary(): binary = "quasar" + (PLATFORM["os"] == "windows" and ".exe" or "") log.info("PLATFORM: %s" % str(PLATFORM)) binary_dir = os.path.join(ADDON_PATH, "resources", "bin", "%(os)s_%(arch)s" % PLATFORM) if PLATFORM["os"] == "android": log.info("Detected binary folder: %s" % binary_dir) binary_dir_legacy = binary_dir.replace("/storage/emulated/0", "/storage/emulated/legacy") if os.path.exists(binary_dir_legacy): binary_dir = binary_dir_legacy log.info("Using binary folder: %s" % binary_dir) app_id = android_get_current_appid() xbmc_data_path = os.path.join("/data", "data", app_id) try: #Test if there is any permisions problem f = open(os.path.join(xbmc_data_path, "test.txt"), "wb") f.write("test") f.close() os.remove(os.path.join(xbmc_data_path, "test.txt")) except: xbmc_data_path = '' if not os.path.exists(xbmc_data_path): log.info("%s path does not exist, so using %s as xbmc_data_path" % (xbmc_data_path, xbmc.translatePath("special://xbmcbin/"))) xbmc_data_path = xbmc.translatePath("special://xbmcbin/") try: #Test if there is any permisions problem f = open(os.path.join(xbmc_data_path, "test.txt"), "wb") f.write("test") f.close() os.remove(os.path.join(xbmc_data_path, "test.txt")) except: xbmc_data_path = '' if not os.path.exists(xbmc_data_path): log.info("%s path does not exist, so using %s as xbmc_data_path" % (xbmc_data_path, xbmc.translatePath("special://masterprofile/"))) xbmc_data_path = xbmc.translatePath("special://masterprofile/") dest_binary_dir = os.path.join(xbmc_data_path, "files", ADDON_ID, "bin", "%(os)s_%(arch)s" % PLATFORM) else: dest_binary_dir = os.path.join(xbmc.translatePath(ADDON.getAddonInfo("profile")).decode('utf-8'), "bin", "%(os)s_%(arch)s" % PLATFORM) log.info("Using destination binary folder: %s" % dest_binary_dir) binary_path = os.path.join(binary_dir, binary) dest_binary_path = os.path.join(dest_binary_dir, binary) if not os.path.exists(binary_path): notify((getLocalizedString(30103) + " %(os)s_%(arch)s" % PLATFORM), time=7000) system_information() try: log.info("Source directory (%s):\n%s" % (binary_dir, os.listdir(os.path.join(binary_dir, "..")))) log.info("Destination directory (%s):\n%s" % (dest_binary_dir, os.listdir(os.path.join(dest_binary_dir, "..")))) except Exception: pass return False, False if os.path.isdir(dest_binary_path): log.warning("Destination path is a directory, expected previous binary file, removing...") try: shutil.rmtree(dest_binary_path) except Exception as e: log.error("Unable to remove destination path for update: %s" % e) system_information() return False, False if not os.path.exists(dest_binary_path) or get_quasard_checksum(dest_binary_path) != get_quasard_checksum(binary_path): log.info("Updating quasar daemon...") try: os.makedirs(dest_binary_dir) except OSError: pass try: shutil.rmtree(dest_binary_dir) except Exception as e: log.error("Unable to remove destination path for update: %s" % e) system_information() pass try: shutil.copytree(binary_dir, dest_binary_dir) except Exception as e: log.error("Unable to copy to destination path for update: %s" % e) system_information() return False, False # Clean stale files in the directory, as this can cause headaches on # Android when they are unreachable dest_files = set(os.listdir(dest_binary_dir)) orig_files = set(os.listdir(binary_dir)) log.info("Deleting stale files %s" % (dest_files - orig_files)) for file_ in (dest_files - orig_files): path = os.path.join(dest_binary_dir, file_) if os.path.isdir(path): shutil.rmtree(path) else: os.remove(path) return dest_binary_dir, ensure_exec_perms(dest_binary_path) def clear_fd_inherit_flags(): # Ensure the spawned quasar binary doesn't inherit open files from Kodi # which can break things like addon updates. [WINDOWS ONLY] from ctypes import windll HANDLE_RANGE = xrange(0, 65536) HANDLE_FLAG_INHERIT = 1 FILE_TYPE_DISK = 1 for hd in HANDLE_RANGE: if windll.kernel32.GetFileType(hd) == FILE_TYPE_DISK: if not windll.kernel32.SetHandleInformation(hd, HANDLE_FLAG_INHERIT, 0): log.error("Error clearing inherit flag, disk file handle %x" % hd) def jsonrpc_enabled(notify=False): try: s = socket.socket() s.connect(('127.0.0.1', 9090)) s.close() log.info("Kodi's JSON-RPC service is available, starting up...") del s return True except Exception as e: log.error(repr(e)) if notify: xbmc.executebuiltin("ActivateWindow(ServiceSettings)") dialog = xbmcgui.Dialog() dialog.ok("Quasar", getLocalizedString(30199)) return False def start_quasard(**kwargs): jsonrpc_failures = 0 while jsonrpc_enabled() is False: jsonrpc_failures += 1 log.warning("Unable to connect to Kodi's JSON-RPC service, retrying...") if jsonrpc_failures > 1: time.sleep(5) if not jsonrpc_enabled(notify=True): log.error("Unable to reach Kodi's JSON-RPC service, aborting...") return False else: break time.sleep(3) quasar_dir, quasar_binary = get_quasar_binary() if quasar_dir is False or quasar_binary is False: return False lockfile = os.path.join(ADDON_PATH, ".lockfile") if os.path.exists(lockfile): log.warning("Existing process found from lockfile, killing...") try: with open(lockfile) as lf: pid = int(lf.read().rstrip(" \t\r\n\0")) os.kill(pid, 9) except Exception as e: log.error(repr(e)) if PLATFORM["os"] == "windows": log.warning("Removing library.db.lock file...") try: library_lockfile = os.path.join(xbmc.translatePath(ADDON.getAddonInfo("profile")).decode('utf-8'), "library.db.lock") os.remove(library_lockfile) except Exception as e: log.error(repr(e)) SW_HIDE = 0 STARTF_USESHOWWINDOW = 1 args = [quasar_binary] kwargs["cwd"] = quasar_dir if PLATFORM["os"] == "windows": args[0] = getWindowsShortPath(quasar_binary) kwargs["cwd"] = getWindowsShortPath(quasar_dir) si = subprocess.STARTUPINFO() si.dwFlags = STARTF_USESHOWWINDOW si.wShowWindow = SW_HIDE clear_fd_inherit_flags() kwargs["startupinfo"] = si else: env = os.environ.copy() env["LD_LIBRARY_PATH"] = "%s:%s" % (quasar_dir, env.get("LD_LIBRARY_PATH", "")) kwargs["env"] = env kwargs["close_fds"] = True wait_counter = 1 while xbmc.getCondVisibility('Window.IsVisible(10140)') or xbmc.getCondVisibility('Window.IsActive(10140)'): if wait_counter == 1: log.info('Add-on settings currently opened, waiting before starting...') if wait_counter > 300: break time.sleep(1) wait_counter += 1 return subprocess.Popen(args, **kwargs) def shutdown(): try: urllib2.urlopen(QUASARD_HOST + "/shutdown") except: pass def wait_for_abortRequested(proc, monitor): monitor.closing.wait() log.info("quasard: exiting quasard daemon") try: proc.terminate() except OSError: pass # Process already exited, nothing to terminate log.info("quasard: quasard daemon exited") def quasard_thread(monitor): crash_count = 0 try: while not xbmc.abortRequested: log.info("quasard: starting quasard") proc = start_quasard(stdout=subprocess.PIPE, stderr=subprocess.STDOUT) if not proc: break threading.Thread(target=wait_for_abortRequested, args=[proc, monitor]).start() if PLATFORM["os"] == "windows": while proc.poll() is None: log.info(proc.stdout.readline()) else: # Kodi hangs on some Android (sigh...) systems when doing a blocking # read. We count on the fact that Quasar daemon flushes its log # output on \n, creating a pretty clean output import fcntl import select fd = proc.stdout.fileno() fl = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) while proc.poll() is None: try: to_read, _, _ = select.select([proc.stdout], [], []) for ro in to_read: line = ro.readline() if line == "": # write end is closed break log.info(line) except IOError: time.sleep(1) # nothing to read, sleep log.info("quasard: proc.return code: %s" % str(proc.returncode)) if proc.returncode == 0 or xbmc.abortRequested: break crash_count += 1 notify(getLocalizedString(30100), time=3000) xbmc.executebuiltin("Dialog.Close(all, true)") system_information() time.sleep(5) if crash_count >= 3: notify(getLocalizedString(30110), time=3000) break except Exception as e: import traceback map(log.error, traceback.format_exc().split("\n")) notify("%s: %s" % (getLocalizedString(30226), repr(e).encode('utf-8'))) raise