Files
addon/plugin.video.alfa/lib/quasar/daemon.py
2019-02-06 16:59:07 +01:00

295 lines
11 KiB
Python

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
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