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"])
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
"/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:
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:
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"):
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"])
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("<Key>", 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()