X-Git-Url: https://glassweightruler.freedombox.rocks/gitweb/waydroid.git/blobdiff_plain/a2804841ed52f812b1d666cb3df01239ee906f5f..094a4d970ccefa2e6409c8147c1edbdaaa74df0f:/tools/actions/initializer.py diff --git a/tools/actions/initializer.py b/tools/actions/initializer.py index 89bbf57..7fec732 100644 --- a/tools/actions/initializer.py +++ b/tools/actions/initializer.py @@ -5,15 +5,15 @@ import os from tools import helpers import tools.config -from tkinter import * -from tkinter import ttk - import sys import threading -class Daemon(threading.Thread): - def __init__(self): - super().__init__() - self.daemon = True +import multiprocessing +import select +import queue +import time +import dbus +import dbus.service +from gi.repository import GLib def is_initialized(args): return os.path.isfile(args.config) and os.path.isdir(tools.config.defaults["rootfs"]) @@ -33,13 +33,16 @@ def setup_config(args): args.arch = helpers.arch.host() cfg["waydroid"]["arch"] = args.arch - preinstalled_images = tools.config.defaults["preinstalled_images_path"] + preinstalled_images_paths = tools.config.defaults["preinstalled_images_paths"] if not args.images_path: - if os.path.isdir(preinstalled_images): - if os.path.isfile(preinstalled_images + "/system.img") and os.path.isfile(preinstalled_images + "/vendor.img"): - args.images_path = preinstalled_images - else: - logging.error("Missing system or vendor on preinstalled images dir, fallback to default") + for preinstalled_images in preinstalled_images_paths: + if os.path.isdir(preinstalled_images): + if os.path.isfile(preinstalled_images + "/system.img") and os.path.isfile(preinstalled_images + "/vendor.img"): + args.images_path = preinstalled_images + break + else: + logging.warning("Found directory {} but missing system or vendor image, ignoring...".format(preinstalled_images)) + if not args.images_path: args.images_path = tools.config.defaults["images_path"] cfg["waydroid"]["images_path"] = args.images_path @@ -58,7 +61,7 @@ def setup_config(args): "/waydroid_" + args.arch + "/" + args.system_type + ".json" system_request = helpers.http.retrieve(args.system_ota) if system_request[0] != 200: - if args.images_path != preinstalled_images: + if args.images_path not in preinstalled_images_paths: raise ValueError( "Failed to get system OTA channel: {}, error: {}".format(args.system_ota, system_request[0])) else: @@ -76,7 +79,7 @@ def setup_config(args): break if not args.vendor_type: - if args.images_path != preinstalled_images: + if args.images_path not in preinstalled_images_paths: raise ValueError( "Failed to get vendor OTA channel: {}".format(vendor_ota)) else: @@ -97,8 +100,13 @@ def setup_config(args): cfg["waydroid"]["hwbinder"] = args.HWBINDER_DRIVER tools.config.save(args, cfg) -def do_init(args): +def init(args): if not is_initialized(args) or args.force: + initializer_service = None + try: + initializer_service = tools.helpers.ipc.DBusContainerService("/Initializer", "id.waydro.Initializer") + except dbus.DBusException: + pass setup_config(args) status = "STOPPED" if os.path.exists(tools.config.defaults["lxc"] + "/waydroid"): @@ -107,7 +115,7 @@ def do_init(args): logging.info("Stopping container") helpers.lxc.stop(args) helpers.images.umount_rootfs(args) - if args.images_path != tools.config.defaults["preinstalled_images_path"]: + if args.images_path not in tools.config.defaults["preinstalled_images_paths"]: helpers.images.get(args) if not os.path.isdir(tools.config.defaults["rootfs"]): os.mkdir(tools.config.defaults["rootfs"]) @@ -119,90 +127,288 @@ def do_init(args): helpers.images.mount_rootfs(args, args.images_path) helpers.lxc.start(args) - helpers.ipc.notify(channel="init", msg="done") + if "running_init_in_service" not in args or not args.running_init_in_service: + try: + if initializer_service: + initializer_service.Done() + except dbus.DBusException: + pass else: logging.info("Already initialized") -def init(args): - if args.gui: - gui_init(args) - else: - do_init(args) - -def gui_init(args): - if is_initialized(args) and not args.force: - return - - root = Tk() - root.title("Initialize Waydroid") - root.iconphoto(True, PhotoImage(file="/usr/lib/waydroid/data/AppIcon.png")) - frm = ttk.Frame(root, padding=10) - frm.grid() - - systemChannel = StringVar(frm, args.system_channel or tools.config.channels_defaults["system_channel"]) - ttk.Label(frm, text="System OTA").grid(row=0, column=0) - ttk.Entry(frm, textvariable=systemChannel).grid(row=0, column=1, ipadx=20) - - vendorChannel = StringVar(frm, args.vendor_channel or tools.config.channels_defaults["vendor_channel"]) - ttk.Label(frm, text="Vendor OTA").grid(row=1, column=0) - ttk.Entry(frm, textvariable=vendorChannel).grid(row=1, column=1, ipadx=20) - - systemType = StringVar(frm) - systemTypes = ["VANILLA", "GAPPS"] - ttk.Label(frm, text="Android Type").grid(row=2, column=0) - ttk.OptionMenu(frm, systemType, args.system_type or systemTypes[0], *systemTypes).grid(row=2, column=1) - - done = ttk.Button(frm, text="Done", command=root.destroy) - - logBox = Text(frm, borderwidth=3, relief="sunken", height=5) - logBox.bind("", lambda e: "break") - - class StdoutRedirect(logging.StreamHandler): - def write(self, s): - if s.startswith('\r'): - logBox.delete("end-1l", "end") - logBox.insert(END, '\n') - s = s[1:] - - logBox.insert(END, s) - logBox.see(END) - def flush(self): +def wait_for_init(args): + helpers.ipc.create_channel("remote_init_output") + + mainloop = GLib.MainLoop() + dbus_obj = DbusInitializer(mainloop, dbus.SystemBus(), '/Initializer', args) + mainloop.run() + + # After init + dbus_obj.remove_from_connection() + +class DbusInitializer(dbus.service.Object): + def __init__(self, looper, bus, object_path, args): + self.args = args + self.looper = looper + dbus.service.Object.__init__(self, bus, object_path) + + @dbus.service.method("id.waydro.Initializer", in_signature='a{ss}', out_signature='', sender_keyword="sender", connection_keyword="conn") + def Init(self, params, sender=None, conn=None): + channels_cfg = tools.config.load_channels() + no_auth = params["system_channel"] == channels_cfg["channels"]["system_channel"] and \ + params["vendor_channel"] == channels_cfg["channels"]["vendor_channel"] + if no_auth or ensure_polkit_auth(sender, conn, "id.waydro.Initializer.Init"): + threading.Thread(target=remote_init_server, args=(self.args, params)).start() + else: + raise PermissionError("Polkit: Authentication failed") + + @dbus.service.method("id.waydro.Initializer", in_signature='', out_signature='') + def Done(self): + if is_initialized(self.args): + self.looper.quit() + +def ensure_polkit_auth(sender, conn, privilege): + dbus_info = dbus.Interface(conn.get_object("org.freedesktop.DBus", "/org/freedesktop/DBus/Bus", False), "org.freedesktop.DBus") + pid = dbus_info.GetConnectionUnixProcessID(sender) + polkit = dbus.Interface(dbus.SystemBus().get_object("org.freedesktop.PolicyKit1", "/org/freedesktop/PolicyKit1/Authority", False), "org.freedesktop.PolicyKit1.Authority") + try: + (is_auth, _, _) = polkit.CheckAuthorization( + ("unix-process", { + "pid": dbus.UInt32(pid, variant_level=1), + "start-time": dbus.UInt64(0, variant_level=1)}), + privilege, {"AllowUserInteraction": "true"}, + dbus.UInt32(1), + "", + timeout=300) + return is_auth + except dbus.DBusException: + raise PermissionError("Polkit: Authentication timed out") + +def background_remote_init_process(args): + with helpers.ipc.open_channel("remote_init_output", "wb") as channel_out: + class StdoutRedirect(logging.StreamHandler): + def write(self, s): + channel_out.write(str.encode(s)) + def flush(self): + pass + def emit(self, record): + if record.levelno >= logging.INFO: + self.write(self.format(record) + self.terminator) + + out = StdoutRedirect() + sys.stdout = sys.stderr = out + logging.getLogger().addHandler(out) + + ctl_queue = queue.Queue() + def try_init(args): + try: + init(args) + except Exception as e: + print(str(e)) + finally: + ctl_queue.put(0) + + def poll_pipe(): + poller = select.poll() + poller.register(channel_out, select.POLLERR) + poller.poll() + # When reaching here the client was terminated + ctl_queue.put(0) + + init_thread = threading.Thread(target=try_init, args=(args,)) + init_thread.daemon = True + init_thread.start() + + poll_thread = threading.Thread(target=poll_pipe) + poll_thread.daemon = True + poll_thread.start() + + # Join any one of the two threads + # Then exit the subprocess to kill the remaining thread. + # Can you believe this is the only way to kill a thread in python??? + ctl_queue.get() + + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + logging.getLogger().removeHandler(out) + +def remote_init_server(args, params): + args.force = True + args.images_path = "" + args.rom_type = "" + args.system_channel = params["system_channel"] + args.vendor_channel = params["vendor_channel"] + args.system_type = params["system_type"] + args.running_init_in_service = True + + p = multiprocessing.Process(target=background_remote_init_process, args=(args,)) + p.daemon = True + p.start() + p.join() + +def remote_init_client(args): + # Local imports cause Gtk is intrusive + import gi + gi.require_version("Gtk", "3.0") + from gi.repository import Gtk + + bus = dbus.SystemBus() + + if is_initialized(args): + try: + tools.helpers.ipc.DBusContainerService("/Initializer", "id.waydro.Initializer").Done() + except dbus.DBusException: pass - def emit(self, record): - if record.levelno >= logging.INFO: - self.write(self.format(record) + self.terminator) - - out = StdoutRedirect() - sys.stdout = sys.stderr = out - logging.getLogger().addHandler(out) - - def runInit(): - download["state"] = DISABLED - logBox.grid(row=4, columnspan=2) - - args.system_channel = systemChannel.get() - args.vendor_channel = vendorChannel.get() - args.system_type = systemType.get() + return - class Runner(Daemon): - def run(self): + def notify_and_quit(caller): + if is_initialized(args): + try: + tools.helpers.ipc.DBusContainerService("/Initializer", "id.waydro.Initializer").Done() + except dbus.DBusException: + pass + GLib.idle_add(Gtk.main_quit) + + class WaydroidInitWindow(Gtk.Window): + def __init__(self): + super().__init__(title="Initialize Waydroid") + channels_cfg = tools.config.load_channels() + + self.set_default_size(600, 250) + self.set_icon_from_file(tools.config.tools_src + "/data/AppIcon.png") + + grid = Gtk.Grid(row_spacing=6, column_spacing=6, margin=10, column_homogeneous=True) + grid.set_hexpand(True) + grid.set_vexpand(True) + self.add(grid) + + sysOtaLabel = Gtk.Label("System OTA") + sysOtaEntry = Gtk.Entry() + sysOtaEntry.set_text(channels_cfg["channels"]["system_channel"]) + grid.attach(sysOtaLabel, 0, 0, 1, 1) + grid.attach_next_to(sysOtaEntry ,sysOtaLabel, Gtk.PositionType.RIGHT, 2, 1) + self.sysOta = sysOtaEntry.get_buffer() + + vndOtaLabel = Gtk.Label("Vendor OTA") + vndOtaEntry = Gtk.Entry() + vndOtaEntry.set_text(channels_cfg["channels"]["vendor_channel"]) + grid.attach(vndOtaLabel, 0, 1, 1, 1) + grid.attach_next_to(vndOtaEntry, vndOtaLabel, Gtk.PositionType.RIGHT, 2, 1) + self.vndOta = vndOtaEntry.get_buffer() + + sysTypeLabel = Gtk.Label("Android Type") + sysTypeCombo = Gtk.ComboBoxText() + sysTypeCombo.set_entry_text_column(0) + for t in ["VANILLA", "GAPPS"]: + sysTypeCombo.append_text(t) + sysTypeCombo.set_active(0) + grid.attach(sysTypeLabel, 0, 2, 1, 1) + grid.attach_next_to(sysTypeCombo, sysTypeLabel, Gtk.PositionType.RIGHT, 2, 1) + self.sysType = sysTypeCombo + + downloadBtn = Gtk.Button("Download") + downloadBtn.connect("clicked", self.on_download_btn_clicked) + grid.attach(downloadBtn, 1,3,1,1) + self.downloadBtn = downloadBtn + + doneBtn = Gtk.Button("Done") + doneBtn.connect("clicked", lambda x: self.destroy()) + doneBtn.get_style_context().add_class('suggested-action') + grid.attach_next_to(doneBtn, downloadBtn, Gtk.PositionType.RIGHT, 1, 1) + self.doneBtn = doneBtn + + outScrolledWindow = Gtk.ScrolledWindow() + outScrolledWindow.set_hexpand(True) + outScrolledWindow.set_vexpand(True) + outTextView = Gtk.TextView() + outTextView.set_property('editable', False) + outTextView.set_property('cursor-visible', False) + outScrolledWindow.add(outTextView) + grid.attach(outScrolledWindow, 0, 4, 3, 1) + self.outScrolledWindow = outScrolledWindow + self.outTextView = outTextView + self.outBuffer = outTextView.get_buffer() + self.outBuffer.create_mark("end", self.outBuffer.get_end_iter(), False) + + self.open_channel = None + + def scroll_to_bottom(self): + self.outTextView.scroll_mark_onscreen(self.outBuffer.get_mark("end")) + + def on_download_btn_clicked(self, widget): + widget.set_sensitive(False) + self.doneBtn.hide() + self.outTextView.show() + init_params = (self.sysOta.get_text(), self.vndOta.get_text(), self.sysType.get_active_text()) + init_runner = threading.Thread(target=self.run_init, args=init_params) + init_runner.daemon = True + init_runner.start() + + def run_init(self, systemOta, vendorOta, systemType): + def draw_sync(s): + if s.startswith('\r'): + last = self.outBuffer.get_iter_at_line(self.outBuffer.get_line_count()-1) + last.backward_char() + self.outBuffer.delete(last, self.outBuffer.get_end_iter()) + self.outBuffer.insert(self.outBuffer.get_end_iter(), s) + self.scroll_to_bottom() + def draw(s): + GLib.idle_add(draw_sync, s) + + if self.open_channel is not None: + self.open_channel.close() + # Wait for other end to reset + time.sleep(1) + + draw("Waiting for waydroid container service...\n") + try: + params = { + "system_channel": self.sysOta.get_text(), + "vendor_channel": self.vndOta.get_text(), + "system_type": self.sysType.get_active_text() + } + tools.helpers.ipc.DBusContainerService("/Initializer", "id.waydro.Initializer").Init(params, timeout=310) + except dbus.DBusException as e: + if e.get_dbus_name() == "org.freedesktop.DBus.Python.PermissionError": + draw(e.get_dbus_message().splitlines()[-1] + "\n") + else: + draw("The waydroid container service is not listening\n") + GLib.idle_add(self.downloadBtn.set_sensitive, True) + return + + with helpers.ipc.open_channel("remote_init_output", "rb") as channel: + self.open_channel = channel + GLib.idle_add(self.downloadBtn.set_sensitive, True) + line = "" try: - do_init(args) - if is_initialized(args): - done.grid(row=5, columnspan=2) - print("Done") - else: - download["state"] = NORMAL - except Exception as e: - print("ERROR: " + str(e)) - download["state"] = NORMAL - - Runner().start() - - download = ttk.Button(frm, text="Download", command=runInit) - download.grid(row=3, columnspan=2) - root.mainloop() - - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - logging.getLogger().removeHandler(out) + while True: + data = channel.read(1) + if len(data) == 0: + draw(line) + break + c = data.decode() + if c == '\r': + draw(line) + line = c + else: + line += c + if c == '\n': + draw(line) + line = "" + except: + draw("\nInterrupted\n") + + if is_initialized(args): + GLib.idle_add(self.doneBtn.show) + draw("Done\n") + + + GLib.set_prgname("Waydroid") + win = WaydroidInitWindow() + win.connect("destroy", notify_and_quit) + + win.show_all() + win.outTextView.hide() + win.doneBtn.hide() + + Gtk.main()