]> glassweightruler.freedombox.rocks Git - waydroid.git/commitdiff
Add first-launch command [v2]
authorAlessandro Astone <ales.astone@gmail.com>
Tue, 19 Jul 2022 09:35:22 +0000 (11:35 +0200)
committerAlessandro Astone <ales.astone@gmail.com>
Sat, 23 Jul 2022 07:56:34 +0000 (09:56 +0200)
data/Waydroid.desktop
debian/control
tools/__init__.py
tools/actions/__init__.py
tools/actions/initializer.py
tools/helpers/arguments.py
tools/helpers/http.py
tools/helpers/ipc.py

index df05c5ff658c50b571dc4d9df02b8288a4696ea6..4a4521dba44460f05e789de192be74020d0ca1b1 100644 (file)
@@ -1,5 +1,5 @@
 [Desktop Entry]
 Type=Application
 Name=Waydroid
-Exec=waydroid show-full-ui
+Exec=waydroid first-launch
 Icon=/usr/lib/waydroid/data/AppIcon.png
index 02798885e29b980d0b7234bc8fa7e28970275021..9c8c3c430b73d7e07c160f752c4afdb03058d0a9 100644 (file)
@@ -15,7 +15,8 @@ Depends: ${misc:Depends},
          ${python3:Depends},
          lxc,
          python3-gbinder,
-         python3-gi
+         python3-gi,
+         gir1.2-gtk-3.0
 Description: Androidâ„¢ application support
  waydroid allows running a separate Androidâ„¢ environment
  confined to a LXC container.
index 2443e6ebb5d6a7a5c2f580bdbca6288094fe5896..65f0efc68be70abf7ecbbecd5b272c07c565ae54 100644 (file)
@@ -31,28 +31,21 @@ def main():
         args.sudo_timer = True
         args.timeout = 1800
 
-        if not actions.initializer.is_initialized(args):
-            if args.action and (args.action != "init" and args.action != "log"):
-                if not args.wait_for_init:
-                    print('ERROR: WayDroid is not initialized, run "waydroid init"')
-                    return 0
+        if os.geteuid() == 0:
+            if not os.path.exists(args.work):
+                os.mkdir(args.work)
+        elif not os.path.exists(args.log):
+            args.log = "/tmp/tools.log"
 
-                print('WayDroid waiting for initialization...')
-                while helpers.ipc.listen(channel="init") != "done":
-                    pass
+        tools_logging.init(args)
 
-            elif os.geteuid() == 0 and args.action == "init":
-                if not os.path.exists(args.work):
-                    os.mkdir(args.work)
+        if not actions.initializer.is_initialized(args) and \
+                args.action and args.action not in ("init", "first-launch", "log"):
+            if args.wait_for_init:
+                actions.wait_for_init(args)
             else:
-                # This branch is taken if:
-                # - waydroid is not yet initialized
-                # - waydroid is invoked with no command or with log
-                if not os.path.exists(args.log):
-                    # The log could have been already created if init was used and failed, if its not the case we use a temporary one
-                    args.log = "/tmp/tools.log"
-
-        tools_logging.init(args)
+                print('ERROR: WayDroid is not initialized, run "waydroid init"')
+                return 0
 
         # Initialize or require config
         if args.action == "init":
@@ -116,6 +109,10 @@ def main():
             helpers.lxc.logcat(args)
         elif args.action == "show-full-ui":
             actions.app_manager.showFullUI(args)
+        elif args.action == "first-launch":
+            actions.remote_init_client(args)
+            if actions.initializer.is_initialized(args):
+                actions.app_manager.showFullUI(args)
         elif args.action == "status":
             actions.status.print_status(args)
         elif args.action == "log":
index 558ed83ab00ec5a7f17e39ce8c520908e673220a..04167334be890bf72806f5e57fe7789ac2f3a3bf 100644 (file)
@@ -1,6 +1,6 @@
 # Copyright 2021 Erfan Abdi
 # SPDX-License-Identifier: GPL-3.0-or-later
-from tools.actions.initializer import init
+from tools.actions.initializer import init, wait_for_init, remote_init_client
 from tools.actions.upgrader import upgrade
 from tools.actions.session_manager import start, stop
 from tools.actions.container_manager import start, stop, freeze, unfreeze
index 158f6bc607499f845713b90912f65ebbe6010f8e..e74a2b797dc118b50a8c72a47b9eb23077bfd5f2 100644 (file)
@@ -5,6 +5,12 @@ import os
 from tools import helpers
 import tools.config
 
+import sys
+import threading
+import multiprocessing
+import select
+import queue
+
 def is_initialized(args):
     return os.path.isfile(args.config) and os.path.isdir(tools.config.defaults["rootfs"])
 
@@ -112,3 +118,230 @@ def init(args):
         helpers.ipc.notify(channel="init", msg="done")
     else:
         logging.info("Already initialized")
+
+def wait_for_init(args):
+    helpers.ipc.create_channel("init")
+    helpers.ipc.create_channel("remote_init_output")
+    while True:
+        print('WayDroid waiting for initialization...')
+        msg = helpers.ipc.read_one(channel="init")
+        if msg == "done":
+            if is_initialized(args):
+                break
+            else:
+                continue
+        if msg.startswith("cmd"):
+            remote_init_server(args, msg)
+            continue
+
+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, cmd):
+    params = cmd.split('\f')[1:]
+    args.force = True
+    args.images_path = ""
+    args.rom_type = ""
+    args.system_channel = params[0]
+    args.vendor_channel = params[1]
+    args.system_type = params[2]
+
+    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, GLib
+
+    if is_initialized(args):
+        helpers.ipc.notify(channel="init", msg="done")
+        return
+
+    def notify_and_quit(caller):
+        if is_initialized(args):
+            helpers.ipc.notify(channel="init", msg="done")
+        GLib.idle_add(Gtk.main_quit)
+
+    class WaydroidInitWindow(Gtk.Window):
+        def __init__(self):
+            super().__init__(title="Initialize Waydroid")
+            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(tools.config.channels_defaults["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(tools.config.channels_defaults["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 re-open
+                tmp = helpers.ipc.open_channel("init", "w", buffering=1)
+                tmp.close()
+
+            draw("Waiting for waydroid container service...\n")
+            try:
+                helpers.ipc.notify_blocking(channel="init", msg="{}\f{}\f{}\f{}".format(
+                    "cmd", self.sysOta.get_text(), self.vndOta.get_text(), self.sysType.get_active_text()))
+            except:
+                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:
+                    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")
+
+
+    win = WaydroidInitWindow()
+    win.connect("destroy", notify_and_quit)
+
+    win.show_all()
+    win.outTextView.hide()
+    win.doneBtn.hide()
+
+    Gtk.main()
index 79ab8533fd107a80b3ab9ce5c1be5b54af93cb6d..fbca8405425bc7bd4d48d802a5056e4dfc7ceaf8 100644 (file)
@@ -104,6 +104,10 @@ def arguments_fullUI(subparser):
     ret = subparser.add_parser("show-full-ui", help="show android full screen in window")
     return ret
 
+def arguments_firstLaunch(subparser):
+    ret = subparser.add_parser("first-launch", help="initialize waydroid and start it")
+    return ret
+
 def arguments_shell(subparser):
     ret = subparser.add_parser("shell", help="run remote shell command")
     ret.add_argument('COMMAND', nargs='?', help="command to run")
@@ -147,6 +151,7 @@ def arguments():
     arguments_app(sub)
     arguments_prop(sub)
     arguments_fullUI(sub)
+    arguments_firstLaunch(sub)
     arguments_shell(sub)
     arguments_logcat(sub)
 
index ff8195779cb833b5800d9fe1d955a7136c808fbe..2aac2b52f6282f4c183ee2e5d9855239a6dc768c 100644 (file)
@@ -105,7 +105,7 @@ def download(args, url, prefix, cache=True, loglevel=logging.INFO,
         tools.helpers.run.user(args, ["rm", path])
 
     # Download the file
-    logging.log(loglevel, "Download " + url)
+    logging.log(loglevel, "Downloading " + url)
     try:
         with urllib.request.urlopen(url) as response:
             with open(path, "wb") as handle:
index fbf522f180a707422f4d52f634c23eb6ff13add4..2ea28e186b1db2691a7a1edfb464f73c3741f505 100644 (file)
@@ -6,21 +6,32 @@ import os
 
 BASE_DIR = "/var/run/"
 
-def listen(channel):
-    pipe = BASE_DIR + "waydroid-" + channel
-    if not os.path.exists(pipe):
-        os.mkfifo(pipe)
-    with open(pipe) as fifo:
+def pipe_for(channel):
+    return BASE_DIR + "waydroid-" + channel
+
+def read_one(channel):
+    with open_channel(channel, "r", 1) as fifo:
         while True:
             data = fifo.read()
             if len(data) != 0:
                 return data
 
+def create_channel(channel):
+    pipe = pipe_for(channel)
+    if not os.path.exists(pipe):
+        os.mkfifo(pipe)
+
+def open_channel(channel, mode, buffering=0):
+    return open(pipe_for(channel), mode, buffering)
+
 def notify(channel, msg):
-    pipe = BASE_DIR + "waydroid-" + channel
     try:
-        fd = os.open(pipe, os.O_WRONLY | os.O_NONBLOCK)
+        fd = os.open(pipe_for(channel), os.O_WRONLY | os.O_NONBLOCK)
         with os.fdopen(fd, "w") as fifo:
             fifo.write(msg)
     except Exception:
         pass
+
+def notify_blocking(channel, msg):
+    with open_channel(channel, "w", 1) as channel:
+        channel.write(msg)