def request(url, params={}, headers={}, data=None, method=None): if params: url = "".join([url, "?", urlencode(params)]) req = urllib2.Request(url) if method: req.get_method = lambda: method req.add_header("User-Agent", USER_AGENT) req.add_header("Accept-Encoding", "gzip") for k, v in headers.items(): req.add_header(k, v) if data: req.add_data(data) try: with closing(urllib2.urlopen(req)) as response: data = response.read() if response.headers.get("Content-Encoding", "") == "gzip": import zlib data = zlib.decompressobj(16 + zlib.MAX_WBITS).decompress(data) response.data = data response.json = lambda: parse_json(data) response.xml = lambda: parse_xml(data) return response except urllib2.HTTPError, e: log.error("http error: %s => %s" % (url, e)) return None, None
def _dispatch_delete(self, objectname): try: self._objects[objectname]._shutdown() except Exception: _log.error("Error when shutting down the object %s:", type(self._objects[objectname])) _log.debug(traceback.format_exc()) del self._objects[objectname]
def close(self): """ Close the connection and the socket. """ if self.connection_status == "closed": return item = { 'abort': True, 'event': threading.Event() } self.write_thread_queue.append(item) self.write_thread_semaphore.release() # notify new item. item['event'].wait(1) if not item['event'].isSet(): _log.warning("write thread doesn't process our abort command") try: self.handler._shutdown() except Exception: _log.error("Error when shutting down the handler: %s", traceback.format_exc()) try: self._sck.shutdown(socket.SHUT_RDWR) except socket.error: pass self._sck.close() self.connection_status = "closed"
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)) 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 return subprocess.Popen(args, **kwargs)
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 _send(self, response): txtResponse = None try: txtResponse = json.dumps(response, self) except Exception as e: _log.error("An unexpected error ocurred when trying to create the message: %r", e) response = {"result": None, "error": "InternalServerError: " + repr(e)} txtResponse = json.dumps(response, self) try: self.write(txtResponse) except TypeError: _log.debug("response was: %r", response) raise
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: dialog = xbmcgui.Dialog() dialog.ok("Quasar", getLocalizedString(30199)) return False
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 _send(self, response): txtResponse = None try: txtResponse = json.dumps(response, self) except Exception as e: _log.error("An unexpected error ocurred when trying to create the message: %r", e) response = { 'result': None, 'error': "InternalServerError: " + repr(e) } txtResponse = json.dumps(response, self) try: self.write(txtResponse) except TypeError: _log.debug("response was: %r", response) raise
def _format_exception(self, obj, method, args, kw, exc): etype, evalue, etb = exc funargs = ", ".join([repr(x) for x in args] + ["%s=%r" % (k, kw[k]) for k in kw]) if len(funargs) > 40: funargs = funargs[:37] + "..." _log.error("(%s) In Handler method %s.%s(%s) ", obj.__class__.__module__, obj.__class__.__name__, method, funargs) _log.debug("\n".join([ "%s::%s:%d %s" % (filename, fnname, lineno, srcline) for filename, lineno, fnname, srcline in traceback.extract_tb(etb)[1:] ])) _log.error("Unhandled error: %s: %s", etype.__name__, evalue) del etb return '%s: %s' % (etype.__name__, evalue)
def _readn(self): """ Internal function which reads from socket waiting for a newline """ streambuffer = self._buffer pos = streambuffer.find(b'\n') # _log.debug("read...") # retry = 0 while pos == -1: data = b'' try: data = self._sck.recv(2048) except IOError as inst: _log.debug("Read socket error: IOError%r (timeout: %r)", inst.args, self._sck.gettimeout()) if inst.errno in (errno.EAGAIN, errno.EWOULDBLOCK): if self._sck.gettimeout() == 0: # if it was too fast self._sck.settimeout(5) continue # time.sleep(0.5) # retry += 1 # if retry < 10: # _log.debug("Retry %s", retry) # continue # _log.debug(traceback.format_exc(0)) if inst.errno in self._SOCKET_COMM_ERRORS: raise EofError(len(streambuffer)) return b'' except socket.error as inst: _log.error("Read socket error: socket.error%r (timeout: %r)", inst.args, self._sck.gettimeout()) # _log.debug(traceback.format_exc(0)) return b'' except: raise if not data: raise EofError(len(streambuffer)) # _log.debug("readbuf+: %r", data) streambuffer += data pos = streambuffer.find(b'\n') self._buffer = streambuffer[pos + 1:] streambuffer = streambuffer[:pos] # _log.debug("read: %r", buffer) return streambuffer
def _readn(self): """ Internal function which reads from socket waiting for a newline """ streambuffer = self._buffer pos = streambuffer.find(b"\n") # _log.debug("read...") # retry = 0 while pos == -1: data = b"" try: data = self._sck.recv(2048) except IOError as inst: _log.debug("Read socket error: IOError%r (timeout: %r)", inst.args, self._sck.gettimeout()) if inst.errno in (errno.EAGAIN, errno.EWOULDBLOCK): if self._sck.gettimeout() == 0: # if it was too fast self._sck.settimeout(5) continue # time.sleep(0.5) # retry += 1 # if retry < 10: # _log.debug("Retry %s", retry) # continue # _log.debug(traceback.format_exc(0)) if inst.errno in self._SOCKET_COMM_ERRORS: raise EofError(len(streambuffer)) return b"" except socket.error as inst: _log.error("Read socket error: socket.error%r (timeout: %r)", inst.args, self._sck.gettimeout()) # _log.debug(traceback.format_exc(0)) return b"" except: raise if not data: raise EofError(len(streambuffer)) # _log.debug("readbuf+: %r", data) streambuffer += data pos = streambuffer.find(b"\n") self._buffer = streambuffer[pos + 1 :] streambuffer = streambuffer[:pos] # _log.debug("read: %r", buffer) return streambuffer
def _format_exception(self, obj, method, args, kw, exc): etype, evalue, etb = exc funargs = ", ".join([repr(x) for x in args] + ["%s=%r" % (k, kw[k]) for k in kw]) if len(funargs) > 40: funargs = funargs[:37] + "..." _log.error( "(%s) In Handler method %s.%s(%s) ", obj.__class__.__module__, obj.__class__.__name__, method, funargs ) _log.debug( "\n".join( [ "%s::%s:%d %s" % (filename, fnname, lineno, srcline) for filename, lineno, fnname, srcline in traceback.extract_tb(etb)[1:] ] ) ) _log.error("Unhandled error: %s: %s", etype.__name__, evalue) del etb return "%s: %s" % (etype.__name__, evalue)
def setresponse(self, value): """ Method used by Connection instance to tell Request that a Response is available to this request. Parameters: **value** Value (JSON decoded) received from socket. """ self.responses.put(value) for callback in self.callbacks: try: callback(self) except Exception as exc: _log.error("Error on callback: %r", exc) _log.debug(traceback.format_exc()) self.event_response.set() # helper for threads. if self.auto_close: self.close()
def close(self): """ Close the connection and the socket. """ if self.connection_status == "closed": return item = {"abort": True, "event": threading.Event()} self.write_thread_queue.append(item) self.write_thread_semaphore.release() # notify new item. item["event"].wait(1) if not item["event"].isSet(): _log.warning("write thread doesn't process our abort command") try: self.handler._shutdown() except Exception: _log.error("Error when shutting down the handler: %s", traceback.format_exc()) try: self._sck.shutdown(socket.SHUT_RDWR) except socket.error: pass self._sck.close() self.connection_status = "closed"
def get_quasar_binary(): binary = "quasar" + (PLATFORM["os"] == "windows" and ".exe" or "") binary_dir = os.path.join(ADDON.getAddonInfo("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) 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")), "bin", "%(os)s_%(arch)s" % PLATFORM) try: binary_dir = binary_dir.decode("latin1") dest_binary_dir = dest_binary_dir.decode("latin1") except UnicodeEncodeError: log.info("Unable to decode: binary_dir=%s dest_binary_dir=%s" % (repr(binary_dir), repr(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(ADDON.getLocalizedString(30103).encode('utf-8')) 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 OSError 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 OSError as e: log.error("Unable to copy to destination path for update: %s" % e) notify(ADDON.getLocalizedString(30227).encode('utf-8')) system_information() pass # 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 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 call_binary(function, cmd, retry=False, p=None, **kwargs): import xbmc import xbmcaddon import traceback import base64 import requests import time import json import xbmcvfs # Lets try first the traditional way if not p: try: p = subprocess.Popen(cmd, **kwargs) log.info('## Executing Popen.CMD: %s - PID: %s' % (cmd, p.pid)) return p except Exception as e: if not PY3: e = unicode(str(e), "utf8", errors="replace").encode("utf8") elif PY3 and isinstance(e, bytes): e = e.decode("utf8") log.error('Exception Popen ERROR: %s, %s' % (str(cmd), str(e))) if PLATFORM["os"] == "android" and ('Errno 13' in str(e) or 'Errno 2' in str(e)): p = None else: return p # The traditional way did not work, so most probably we hit the SDK 29 problem APP_PARAMS = { 'Quasar': { 'ACTIVE': 0, 'USER_ADDON': 'plugin.video.quasar', 'USER_ADDON_STATUS': xbmc.getCondVisibility('System.HasAddon("plugin.video.quasar")'), 'USER_ADDON_USERDATA': os.path.join(xbmc.translatePath('special://masterprofile/'), 'addon_data', 'plugin.video.quasar'), 'USER_APK': [os.path.join(xbmc.translatePath('special://xbmcbinaddons/'), 'plugin.video.quasar', 'resources', 'apk', 'quasar-mobile-assistant.apk')], 'USER_APP': 'com.quasar.quasarmobileassistant', 'USER_APP_CONTROL': 'quasar-mobile-assistant.version', 'USER_APP_URL': 'http://127.0.0.1', 'USER_APP_PORT': '66666', 'USER_APP_PORT_ALT': '66667' }, 'Alfa': { 'ACTIVE': 1, 'USER_ADDON': 'plugin.video.alfa', 'USER_ADDON_STATUS': xbmc.getCondVisibility('System.HasAddon("plugin.video.alfa")'), 'USER_ADDON_USERDATA': os.path.join(xbmc.translatePath('special://masterprofile/'), 'addon_data', 'plugin.video.alfa'), 'USER_APK': ['https://github.com/alfa-addon/alfa-repo/raw/master/downloads/assistant/alfa-mobile-assistant.apk', 'https://github.com/alfa-addon/alfa-repo/raw/master/downloads/assistant/alfa-mobile-assistant.version'], 'USER_APP': 'com.alfa.alfamobileassistant', 'USER_APP_CONTROL': 'alfa-mobile-assistant.version', 'USER_APP_URL': 'http://127.0.0.1', 'USER_APP_PORT': '48885', 'USER_APP_PORT_ALT': '48886' } } QUASAR_ADDON_SETTING = ADDON QUASAR_ADDON_USERDATA = translatePath(QUASAR_ADDON_SETTING.getAddonInfo("profile")) if xbmc.getCondVisibility('System.HasAddon("plugin.video.torrest")'): TORREST_ADDON = True else: TORREST_ADDON = False USER_APP_URL = '' USER_APP_URL_ALT = '' USER_ADDON = '' USER_ADDON_STATUS = False ANDROID_STORAGE = os.getenv('ANDROID_STORAGE') if not ANDROID_STORAGE: ANDROID_STORAGE = '/storage' USER_APP = '' USER_APP_PATH = os.path.join(ANDROID_STORAGE, 'emulated', '0', 'Android', 'data') USER_APP_STATUS = False for user_addon, user_params in list(APP_PARAMS.items()): if not user_params['ACTIVE'] or not user_params['USER_ADDON_STATUS']: continue if user_addon == 'Quasar': try: # Quasar add-on and Quasar Assistant installed USER_ADDON = user_params['USER_ADDON'] USER_ADDON_STATUS = True if os.path.exists(os.path.join(USER_APP_PATH, user_params['USER_APP'])): USER_APP = user_params['USER_APP'] USER_APP_STATUS = True USER_APP_URL = "%s:%s" % (user_params['USER_APP_URL'], user_params['USER_APP_PORT']) USER_APP_URL_ALT = "%s:%s" % (user_params['USER_APP_URL'], user_params['USER_APP_PORT_ALT']) if USER_APP_STATUS: break except: log.error(traceback.format_exc()) if user_addon == 'Alfa': try: try: # Alfa add-on and Alfa Assistant installed USER_ADDON_SETTING = xbmcaddon.Addon(id="%s" % user_params['USER_ADDON']) USER_ADDON = user_params['USER_ADDON'] USER_ADDON_STATUS = True if USER_ADDON_SETTING.getSetting('assistant_mode') == 'este' and \ os.path.exists(os.path.join(USER_APP_PATH, user_params['USER_APP'])): USER_APP = user_params['USER_APP'] USER_APP_STATUS = True if USER_ADDON_SETTING.getSetting('assistant_custom_address'): USER_APP_URL_base = "http://%s" % USER_ADDON_SETTING.getSetting('assistant_custom_address') else: USER_APP_URL_base = user_params['USER_APP_URL'] USER_APP_URL = "%s:%s" % (USER_APP_URL_base, user_params['USER_APP_PORT']) USER_APP_URL_ALT = "%s:%s" % (USER_APP_URL_base, user_params['USER_APP_PORT_ALT']) except: # Only Alfa Assistant installed and not Alfa if os.path.exists(os.path.join(USER_APP_PATH, user_params['USER_APP'])): USER_APP_STATUS = True if USER_APP_STATUS: break except: log.error(traceback.format_exc()) if not USER_APP_STATUS: user_params = install_app(APP_PARAMS) if not user_params.get('USER_APP', '') or not user_params.get('USER_APP_URL', '') \ or not user_params.get('USER_APP_PORT', '') or not user_params.get('USER_APP_PORT_ALT', ''): raise ValueError("No app: One MUST be installed") else: USER_APP_STATUS = True USER_APP = user_params['USER_APP'] USER_APP_URL = "%s:%s" % (user_params['USER_APP_URL'], user_params['USER_APP_PORT']) USER_APP_URL_ALT = "%s:%s" % (user_params['USER_APP_URL'], user_params['USER_APP_PORT_ALT']) if USER_APP_STATUS: try: try: # Special process for Android 11+: setting download paths in a "free zone" os_release = 0 if PY3: FF = b'\n' else: FF = '\n' try: for label_a in subprocess.check_output('getprop').split(FF): if PY3 and isinstance(label_a, bytes): label_a = label_a.decode() if 'build.version.release' in label_a: os_release = int(re.findall(':\s*\[(.*?)\]$', label_a, flags=re.DOTALL)[0]) break except: try: with open(os.environ['ANDROID_ROOT'] + '/build.prop', "r") as f: labels = read(f) for label_a in labels.split(): if PY3 and isinstance(label_a, bytes): label_a = label_a.decode() if 'build.version.release' in label_a: os_release = int(re.findall('=(.*?)$', label_a, flags=re.DOTALL)[0]) break except: os_release = 10 log.info(os_release) # If Android 11+, reset the download & torrents paths to /storage/emulated/0/Download/Kodi/Quasar/... if os_release >= 11: if 'userdata/addon_data/plugin.video.' in QUASAR_ADDON_SETTING.getSetting('download_path') \ or not QUASAR_ADDON_SETTING.getSetting('download_path'): download_path = '/storage/emulated/0/Download/Kodi/Quasar/downloads/' QUASAR_ADDON_SETTING.setSetting('download_path', download_path) if not os.path.exists(download_path): res = xbmcvfs.mkdirs(download_path) if 'userdata/addon_data/plugin.video.' in QUASAR_ADDON_SETTING.getSetting('library_path') \ or not QUASAR_ADDON_SETTING.getSetting('library_path'): library_path = '/storage/emulated/0/Download/Kodi/Quasar/Library/' QUASAR_ADDON_SETTING.setSetting('library_path', library_path) if not os.path.exists(library_path): res = xbmcvfs.mkdirs(library_path) except: log.info('## Assistant checking download_path: ERROR on processing') log.info(traceback.format_exc()) """ Assistant APP acts as a CONSOLE for binaries management in Android 10+ and Kodi 19+ Syntax StartAndroidActivity("USER_APP", "", "function", "cmd|arg| ... |arg|||dict{env} in format |key=value|... "): - cmd: binary name in format '$PWD/lib'binary_name'.so' - 'open': START the Assitant - 'terminate': CLOSE Assistant - "OpenBinary", "$PWD/libBINARY.so|-port|61235|-settings|/storage/emulated/.../settings.json||| (kwargs[env]): |key=value|etc=etc": CALL binary Syntax Http requests: http://127.0.0.1:48884/command?option=value - /openBinary?cmd=base64OfFullCommand($PWD/libBINARY.so|-port|61235| -settings|/storage/emulated/.../settings.json||| (kwargs[env]): |key=value|etc=etc): CALL binary - returns: { "pid": 999, "output": "Base64encoded", "error": "Base64encoded", "startDate": "Base64encoded(2021-12-13 14:00:12)", "endDate": "Base64encoded([2021-12-13 14:00:12])", If ended "retCode": "0|1|number|None" None if not ended "cmd": "Base64encoded(command *args **kwargs)" Full command as sent to the app "finalCmd": "Base64encoded($PWD/command *args)" Actual command executed vy the app - /openBinary?cmd=base64OfFullCommand([[ANY Android/Linux command: killall libquasar.so (kills all Quasar binaries)]]) - returns: {As in /openBinary?cmd} - /getBinaryStatus?pid=999: Request binary STATUS by PID - returns: {As in /openBinary?cmd} - /killBinary?pid=999: Request KILL binary PID - returns: {As in /openBinary?cmd} - /getBinaryList: Return a /getBinaryStatus per binary launched during app session - returns: {As in /openBinary?cmd} - /terminate: Close Assistant """ # Lets start the Assistant app separator = '|' separator_escaped = '\|' separator_kwargs = '|||' command = [] status_code = 0 cmd_app = '' url = '' url_open = USER_APP_URL + '/openBinary?cmd=' url_killall = url_open + base64.b64encode(str('killall%slibquasar.so' % separator).encode('utf8')).decode('utf8') if isinstance(p, int): url_killall = USER_APP_URL + '/killBinary?pid=%s' % p cmd_android = 'StartAndroidActivity("%s", "", "%s", "%s")' % (USER_APP, 'open', 'about:blank') cmd_android_close = 'StartAndroidActivity("%s", "", "%s", "%s")' % (USER_APP, 'terminate', 'about:blank') if TORREST_ADDON: time.sleep(10) # let Torrest starts first xbmc.executebuiltin(cmd_android) time.sleep(3) # Build the command & params if isinstance(cmd, list): command.append(cmd) command.append(kwargs) # Convert Args to APP format cmd_bis = cmd[:] cmd_bis[0] = '$PWD/libquasar.so' for args in cmd_bis: cmd_app += args.replace(separator, separator_escaped) + separator cmd_app = cmd_app.rstrip(separator) # Convert Kwargs to APP format if kwargs.get('env', {}): cmd_app += separator_kwargs for key, value in list(kwargs.get('env', {}).items()): if key == 'LD_LIBRARY_PATH': # The app will replace $PWD by the binary/lib path value = '$PWD' if key == 'PATH': # The app will replace $PWD by the binary/lib path value = '$PWD:%s' % value cmd_app += '%s=%s%s' % (key.replace(separator, separator_escaped), \ value.replace(separator, separator_escaped), separator) cmd_app = cmd_app.rstrip(separator) command_base64 = base64.b64encode(cmd_app.encode('utf8')).decode('utf8') else: command_base64 = cmd cmd = p.args_ kwargs = p.kwargs_ command.append(cmd) command.append(kwargs) # Launch the Binary launch_status = True try: session = requests.Session() # First, cancel existing Binary sessions url = url_killall log.info('## Killing Quasar from Assistant App: %s' % url) resp = session.get(url, timeout=5) status_code = resp.status_code if status_code != 200: log.info('## ERROR Killing Quasar from Assistant App: %s' % resp.content) time.sleep(1) # Now lets launch the Binary log.info('## Calling Quasar from Assistant App: %s - Retry = %s' % (cmd, retry)) url = url_open + command_base64 resp = session.get(url, timeout=5) except Exception as e: resp = requests.Response() resp.status_code = str(e) status_code = resp.status_code if status_code != 200 and not retry: log.error("## Calling/Killing Quasar: Invalid app requests response: %s. Closing Assistant (1)" % status_code) if TORREST_ADDON: time.sleep(10) # let Torrest starts first else: xbmc.executebuiltin(cmd_android_close) time.sleep(5) return call_binary(function, cmd, retry=True, **kwargs) elif status_code != 200 and retry: log.error("## Calling/Killing Quasar: Invalid app requests response: %s. Closing Assistant (1)" % status_code) launch_status = False xbmc.executebuiltin(cmd_android_close) time.sleep(5) try: app_response = resp.content if launch_status: if PY3 and isinstance(app_response, bytes): app_response = app_response.decode() app_response = re.sub('\n|\r|\t', '', app_response) app_response = json.loads(app_response) except: status_code = resp.content launch_status = False log.info("## Calling Quasar: Invalid app response: %s" % resp.content) # Simulate the response from subprocess.Popen pipeout, pipein = os.pipe() class Proc: pid = 999999 stdout = os.fdopen(pipeout, 'rb') stdin = os.fdopen(pipein, 'wb') stderr = stdout returncode = None startDate = '' endDate = '' poll = '' terminate = '' communicate = '' app = USER_APP url_app = USER_APP_URL url_app_alt = USER_APP_URL_ALT cmd_app = command_base64 finalCmd = '' args_ = cmd kwargs_ = kwargs sess = session monitor = xbmc.Monitor() binary_time = time.time() binary_awake = 0 torrest = TORREST_ADDON p = Proc() def redirect_terminate(p=p, action='killBinary'): return binary_stat(p, action) def redirect_poll(p=p, action='poll'): return binary_stat(p, action) def redirect_communicate(p=p, action='communicate'): return binary_stat(p, action) p.poll = redirect_poll p.terminate = redirect_terminate p.communicate = redirect_communicate # If something went wrong on the binary launch, lets return the error so it is recovered from the standard code if not launch_status: p.returncode = 999 raise ValueError("No app response: error code: %s" % status_code) try: p.pid = int(app_response['pid']) except: raise ValueError("No valid PID returned: PID code: %s" % resp.content) # Handle initial messages time.sleep(2) if app_response.get('output') or app_response.get('error'): res = binary_stat(p, 'poll', app_response=app_response) else: for x in range(20): res = binary_stat(p, 'poll', init=True) if res: break time.sleep(1) else: # Is the binary hung? Lets restart it return call_binary(function, command_base64, retry=True, kwargs={}) log.info('## Assistant executing CMD: %s - PID: %s' % (command[0], p.pid)) #log.warning('## Assistant executing CMD **kwargs: %s' % command[1]) except: log.info('## Assistant ERROR %s in CMD: %s%s' % (status_code, url, command)) log.error(traceback.format_exc()) return p
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 get_quasar_binary(): binary = "quasar" + (PLATFORM["os"] == "windows" and ".exe" or "") binary_dir = os.path.join(ADDON.getAddonInfo("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) 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")), "bin", "%(os)s_%(arch)s" % PLATFORM) try: binary_dir = binary_dir.decode("latin1") dest_binary_dir = dest_binary_dir.decode("latin1") except UnicodeEncodeError: log.info("Unable to decode: binary_dir=%s dest_binary_dir=%s" % (repr(binary_dir), repr(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(ADDON.getLocalizedString(30103).encode('utf-8')) 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 OSError 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 OSError as e: log.error("Unable to copy to destination path for update: %s" % e) notify(ADDON.getLocalizedString(30227).encode('utf-8')) system_information() pass # 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 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 app_id = android_get_current_appid() xbmc_data_path = translatePath("special://xbmcbin/").replace('user/0', 'data').replace('cache/apk/assets', 'files/quasar') log.info("Trying binary Kodi folder: %s" % xbmc_data_path) try: #Test if there is any permisions problem if not os.path.exists(xbmc_data_path): os.makedirs(xbmc_data_path) except Exception as e: log.info("ERROR %s in binary Kodi folder: %s" % (str(e), xbmc_data_path)) if not os.path.exists(xbmc_data_path): xbmc_data_path = translatePath("special://xbmcbin/").replace('cache/apk/assets', 'files/quasar') log.info("Trying alternative binary Kodi folder: %s" % xbmc_data_path) try: #Test if there is any permisions problem if not os.path.exists(xbmc_data_path): os.makedirs(xbmc_data_path) except Exception as e: log.info("ERROR %s in alternative binary Kodi folder: %s" % (str(e), xbmc_data_path)) dest_binary_dir = xbmc_data_path else: if not PY3: dest_binary_dir = os.path.join(translatePath(ADDON.getAddonInfo("profile")).decode('utf-8'), "bin", "%(os)s_%(arch)s" % PLATFORM) else: dest_binary_dir = os.path.join(translatePath(ADDON.getAddonInfo("profile")), "bin", "%(os)s_%(arch)s" % PLATFORM) if PY3 and isinstance(dest_binary_dir, bytes): dest_binary_dir = dest_binary_dir.decode("utf8") 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() #return False, False 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 if os.path.exists(dest_binary_dir): try: 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) except: pass return dest_binary_dir, ensure_exec_perms(dest_binary_path)
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)'): if wait_counter == 1: log.info('Add-on settings currently opened, waiting before starting...') if wait_counter > 300: break time.sleep(1) return subprocess.Popen(args, **kwargs)
def binary_stat(p, action, retry=False, init=False, app_response={}): if init: log.info('## Binary_stat: action: %s; PID: %s; retry: %s; init: %s; awake: %s; app_r: %s' % \ (action, p.pid, retry, init, p.binary_awake, app_response)) import traceback import base64 import requests import json import time import xbmc try: if action in ['poll', 'communicate']: url = p.url_app + '/getBinaryStatus?pid=%s&flushAfterRead=true' % str(p.pid) url_alt = p.url_app_alt + '/getBinaryStatus?pid=%s&flushAfterRead=true' % str(p.pid) if action == 'killBinary': url = p.url_app + '/killBinary?pid=%s' % str(p.pid) url_alt = p.url_app_alt + '/killBinary?pid=%s' % str(p.pid) url_close = p.url_app + '/terminate' cmd_android = 'StartAndroidActivity("%s", "", "%s", "%s")' % (p.app, 'open', 'about:blank') cmd_android_quit = 'StartAndroidActivity("%s", "", "%s", "%s")' % (p.app, 'quit', 'about:blank') cmd_android_close = 'StartAndroidActivity("%s", "", "%s", "%s")' % (p.app, 'terminate', 'about:blank') cmd_android_permissions = 'StartAndroidActivity("%s", "", "%s", "%s")' % (p.app, 'checkPermissions', 'about:blank') finished = False retry_req = False retry_app = False stdout_acum = '' stderr_acum = '' msg = '' binary_awake = 0 binary_awake_safe = 300*1000 while not finished: if not app_response.get('retCode', 0) >= 999: try: resp = p.sess.get(url, timeout=5) except Exception as e: resp = requests.Response() resp.status_code = str(e) if resp.status_code != 200 and not retry_req: if action == 'killBinary' or p.monitor.abortRequested(): app_response = {'pid': p.pid, 'retCode': 998} else: log.error("## Binary_stat: Invalid app requests response for PID: %s: %s - retry: %s - awake: %s" % \ (p.pid, resp.status_code, retry_req, p.binary_awake)) retry_req = True url = url_alt msg += str(resp.status_code) stdout_acum += str(resp.status_code) xbmc.executebuiltin(cmd_android) binary_awake = (int(time.time()) - int(p.binary_time)) * 1000 - binary_awake_safe if binary_awake < binary_awake_safe: binary_awake = 0 log.info('## Time.awake: %s; binary_awake: %s; p.binary_awake: %s' % \ ((int(time.time()) - int(p.binary_time))*1000, binary_awake, p.binary_awake)) time.sleep(5) continue if resp.status_code != 200 and retry_req and app_response.get('retCode', 0) != 999: log.error("## Binary_stat: Invalid app requests response for PID: %s: %s - retry: %s - awake: %s. Closing Assistant" % \ (p.pid, resp.status_code, retry_req, p.binary_awake)) msg += str(resp.status_code) stdout_acum += str(resp.status_code) app_response = {'pid': p.pid, 'retCode': 999} if p.torrest: time.sleep(10) # let Torrest recover first else: xbmc.executebuiltin(cmd_android_close) time.sleep(5) xbmc.executebuiltin(cmd_android) if binary_awake > binary_awake_safe: if p.binary_awake: if binary_awake < p.binary_awake: p.binary_awake = binary_awake else: p.binary_awake = binary_awake time.sleep(5) log.info('## Time.awake: %s; binary_awake: %s; p.binary_awake: %s' % \ ((int(time.time()) - int(p.binary_time))*1000, binary_awake, p.binary_awake)) try: if not 'awakingInterval' in url: url += '&awakingInterval=%s' % p.binary_awake resp = p.sess.get(url, timeout=5) except: pass time.sleep(1) continue time.sleep(5) continue if resp.status_code == 200: try: app_response = resp.content if PY3 and isinstance(app_response, bytes): app_response = app_response.decode() app_response_save = app_response app_response = re.sub('\n|\r|\t', '', app_response) app_response = json.loads(app_response) test_json = app_response["pid"] except: status_code = resp.content log.error("## Binary_stat: Invalid app response for PID: %s: %s - retry: %s - awake: %s" % \ (p.pid, resp.content, retry_app, p.binary_awake)) if retry_app: app_response = {'pid': p.pid} app_response['retCode'] = 999 msg += app_response_save stdout_acum += app_response_save else: retry_app = True app_response = {} if not p.torrest: if not 'awakingInterval' in url and (binary_awake > 0 or p.binary_awake > 0): if p.binary_awake: if binary_awake and binary_awake < p.binary_awake: p.binary_awake = binary_awake else: p.binary_awake = binary_awake url += '&awakingInterval=%s' % p.binary_awake log.info('## Time.awake: %s; binary_awake: %s; p.binary_awake: %s' % \ ((int(time.time()) - int(p.binary_time))*1000, binary_awake, p.binary_awake)) time.sleep(1) continue if app_response.get("pid", 0): if app_response.get('output'): stdout_acum += base64.b64decode(app_response['output']).decode('utf-8') msg += base64.b64decode(app_response['output']).decode('utf-8') if app_response.get('error'): stderr_acum += base64.b64decode(app_response['error']).decode('utf-8') msg += base64.b64decode(app_response['error']).decode('utf-8') if app_response.get('startDate'): p.startDate = base64.b64decode(app_response['startDate']).decode('utf-8') if app_response.get('endDate'): p.endDate = base64.b64decode(app_response['endDate']).decode('utf-8') if app_response.get('cmd'): p.cmd_app = base64.b64decode(app_response['cmd']).decode('utf-8') if app_response.get('finalCmd'): p.finalCmd = base64.b64decode(app_response['finalCmd']).decode('utf-8') # If still app permissions not allowed, give it a retry if 'permission denied' in msg: notify('Accept Assitant permissions', time=15000) time.sleep(5) xbmc.executebuiltin(cmd_android_permissions) time.sleep(10) xbmc.executebuiltin(cmd_android_quit) time.sleep(5) if msg: try: for line in msg.split('\n'): line += '\n' if PY3 and not isinstance(line, (bytes, bytearray)): line = line.encode('utf-8') p.stdin.write(line) p.stdin.flush() except: pass p.returncode = None if action == 'killBinary' and not app_response.get('retCode', ''): app_response['retCode'] = 137 if app_response.get('retCode', '') or action == 'killBinary' or \ (action == 'communicate' and app_response.get('retCode', '') != ''): try: p.stdin.flush() p.stdin.close() except: pass try: p.returncode = int(app_response['retCode']) except: p.returncode = app_response['retCode'] if action == 'communicate' and p.returncode is not None: log.info("## Binary_stat: communicate Quasar: %s - Returncode: %s" % (p.pid, p.returncode)) return stdout_acum, stderr_acum elif action == 'poll': if init and msg: #log.warning('## Binary_stat: Quasar initial response: %s' % msg) return True return p.returncode elif action == 'killBinary': log.info("## Binary_stat: killBinary Quasar: %s - Returncode: %s" % (p.pid, p.returncode)) try: if p.monitor.abortRequested(): if not p.torrest: try: resp_t = p.sess.get(url_close, timeout=1) except: pass elif p.returncode == 998: if not p.torrest: xbmc.executebuiltin(cmd_android_close) time.sleep(10) except: logging.info(traceback.format_exc()) time.sleep(1) if not p.torrest: xbmc.executebuiltin(cmd_android_close) time.sleep(2) return p time.sleep(5) msg = '' app_response = {} except: log.error(traceback.format_exc()) return None
def install_app(APP_PARAMS): import traceback import requests import xbmc import time try: user_params = {} apk_OK = False ANDROID_STORAGE = os.getenv('ANDROID_STORAGE') if not ANDROID_STORAGE: ANDROID_STORAGE = '/storage' LOCAL_DOWNLOAD_PATH = os.path.join(ANDROID_STORAGE, 'emulated', '0', 'Download') USER_APP_PATH = os.path.join(ANDROID_STORAGE, 'emulated', '0', 'Android', 'data') for user_addon, user_params in list(APP_PARAMS.items()): if not user_params['ACTIVE']: continue for apk_path in user_params['USER_APK']: if apk_path.endswith('.apk'): download_path = LOCAL_DOWNLOAD_PATH elif user_params['USER_ADDON_STATUS']: download_path = user_params['USER_ADDON_USERDATA'] else: continue if apk_path.startswith('http'): try: apk_body = requests.get(apk_path, timeout=10) except Exception as e: apk_body = requests.Response() apk_body.status_code = str(e) if apk_body.status_code != 200: log.error("## Install_app: Invalid app requests response: %s" % (apk_body.status_code)) apk_OK = False continue with open(os.path.join(download_path, \ os.path.basename(apk_path)), "wb") as f: f.write(apk_body.content) apk_OK = True else: if os.path.exists(apk_path): shutil.copy(apk_path, download_path) apk_OK = True else: continue if not apk_OK: break if apk_OK: log.info("## Install_app: Installing the APK from: %s" % LOCAL_DOWNLOAD_PATH) notify('Install your Assistant %s from folder %s' % \ (os.path.basename(user_params['USER_APK'][0]), \ LOCAL_DOWNLOAD_PATH)) cmd_android = 'StartAndroidActivity("%s", "", "%s", "%s")' % (user_params['USER_APP'], 'open', 'about:blank') cmd_android_permissions = 'StartAndroidActivity("%s", "", "%s", "%s")' % (user_params['USER_APP'], 'checkPermissions', 'about:blank') cmd_android_close = 'StartAndroidActivity("%s", "", "%s", "%s")' % (user_params['USER_APP'], 'terminate', 'about:blank') xbmc.executebuiltin(cmd_android) time.sleep(1) # Lets give the user 5 minutes to install the app an retry automatically for x in range(300): if os.path.exists(os.path.join(USER_APP_PATH, user_params['USER_APP'])): log.info("## Install_app: APP installed: %s" % user_params['USER_APP']) notify('Accept Assistant permissions') log.info("## Install_app: Requesting permissions: %s" % user_params['USER_APP']) time.sleep(5) xbmc.executebuiltin(cmd_android_permissions) time.sleep(15) log.info("## Install_app: closing APP: %s" % user_params['USER_APP']) notify('Accept Assistant permissions') xbmc.executebuiltin(cmd_android_close) log.info("## Install_app: APP closed: %s" % user_params['USER_APP']) time.sleep(10) return user_params xbmc.executebuiltin(cmd_android) time.sleep(1) break except: log.info(traceback.format_exc()) user_params = {} return user_params