From 55e0266795bc09436ea45c3ead59d2925762d5c2 Mon Sep 17 00:00:00 2001 From: Alessandro Astone Date: Tue, 19 Jul 2022 11:35:22 +0200 Subject: [PATCH] Add first-launch command [v2] --- data/Waydroid.desktop | 2 +- debian/control | 3 +- tools/__init__.py | 35 +++--- tools/actions/__init__.py | 2 +- tools/actions/initializer.py | 233 +++++++++++++++++++++++++++++++++++ tools/helpers/arguments.py | 5 + tools/helpers/http.py | 2 +- tools/helpers/ipc.py | 25 ++-- 8 files changed, 277 insertions(+), 30 deletions(-) diff --git a/data/Waydroid.desktop b/data/Waydroid.desktop index df05c5f..4a4521d 100644 --- a/data/Waydroid.desktop +++ b/data/Waydroid.desktop @@ -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 diff --git a/debian/control b/debian/control index 0279888..9c8c3c4 100644 --- a/debian/control +++ b/debian/control @@ -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. diff --git a/tools/__init__.py b/tools/__init__.py index 2443e6e..65f0efc 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -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": diff --git a/tools/actions/__init__.py b/tools/actions/__init__.py index 558ed83..0416733 100644 --- a/tools/actions/__init__.py +++ b/tools/actions/__init__.py @@ -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 diff --git a/tools/actions/initializer.py b/tools/actions/initializer.py index 158f6bc..e74a2b7 100644 --- a/tools/actions/initializer.py +++ b/tools/actions/initializer.py @@ -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() diff --git a/tools/helpers/arguments.py b/tools/helpers/arguments.py index 79ab853..fbca840 100644 --- a/tools/helpers/arguments.py +++ b/tools/helpers/arguments.py @@ -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) diff --git a/tools/helpers/http.py b/tools/helpers/http.py index ff81957..2aac2b5 100644 --- a/tools/helpers/http.py +++ b/tools/helpers/http.py @@ -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: diff --git a/tools/helpers/ipc.py b/tools/helpers/ipc.py index fbf522f..2ea28e1 100644 --- a/tools/helpers/ipc.py +++ b/tools/helpers/ipc.py @@ -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) -- 2.47.3