Files
waydroid/tools/actions/initializer.py
2026-03-29 16:52:15 +02:00

460 lines
18 KiB
Python

# Copyright 2021 Erfan Abdi
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import os
from tools import helpers
import tools.config
import stat
import sys
import threading
import multiprocessing
import select
import queue
import time
import dbus
import dbus.service
import argparse
from gi.repository import GLib
def is_initialized(args):
return os.path.isfile(args.config) and os.path.isdir(tools.config.defaults["rootfs"])
def get_vendor_type(args):
vndk_str = helpers.props.host_get(args, "ro.vndk.version")
ret = "MAINLINE"
if vndk_str != "":
vndk = int(vndk_str)
if vndk > 19:
halium_ver = vndk - 19
if vndk > 31:
halium_ver -= 1 # 12L -> Halium 12
ret = "HALIUM_" + str(halium_ver)
if vndk == 32:
ret += "L"
return ret
def setup_config(args):
cfg = tools.config.load(args)
args.arch = helpers.arch.host()
cfg["waydroid"]["arch"] = args.arch
args.vendor_type = get_vendor_type(args)
cfg["waydroid"]["vendor_type"] = args.vendor_type
helpers.drivers.setupBinderNodes(args)
cfg["waydroid"]["binder"] = args.BINDER_DRIVER
cfg["waydroid"]["vndbinder"] = args.VNDBINDER_DRIVER
cfg["waydroid"]["hwbinder"] = args.HWBINDER_DRIVER
has_preinstalled_images = False
preinstalled_images_paths = tools.config.defaults["preinstalled_images_paths"]
for preinstalled_images in preinstalled_images_paths:
if os.path.isdir(preinstalled_images):
system_path = preinstalled_images + "/system.img"
vendor_path = preinstalled_images + "/vendor.img"
system_exists = os.path.isfile(system_path) or stat.S_ISBLK(os.stat(system_path).st_mode)
vendor_exists = os.path.isfile(vendor_path) or stat.S_ISBLK(os.stat(vendor_path).st_mode)
if system_exists and vendor_exists:
has_preinstalled_images = True
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
if has_preinstalled_images:
cfg["waydroid"]["system_ota"] = args.system_ota = "None"
cfg["waydroid"]["vendor_ota"] = args.vendor_ota = "None"
cfg["waydroid"]["system_datetime"] = tools.config.defaults["system_datetime"]
cfg["waydroid"]["vendor_datetime"] = tools.config.defaults["vendor_datetime"]
tools.config.save(args, cfg)
return True
channels_cfg = tools.config.load_channels()
if not args.system_channel:
args.system_channel = channels_cfg["channels"]["system_channel"]
if not args.vendor_channel:
args.vendor_channel = channels_cfg["channels"]["vendor_channel"]
if not args.rom_type:
args.rom_type = channels_cfg["channels"]["rom_type"]
if not args.system_type:
args.system_type = channels_cfg["channels"]["system_type"]
if not args.system_channel or not args.vendor_channel:
logging.error("ERROR: You must provide 'System OTA' and 'Vendor OTA' URLs.")
return False
args.system_ota = args.system_channel + "/" + args.rom_type + \
"/waydroid_" + args.arch + "/" + args.system_type + ".json"
system_request = helpers.http.retrieve(args.system_ota)
if system_request[0] != 200:
raise ValueError(
"Failed to get system OTA channel: {}, error: {}".format(args.system_ota, system_request[0]))
device_codename = helpers.props.host_get(args, "ro.product.device")
args.vendor_type = None
for vendor in [device_codename, get_vendor_type(args)]:
vendor_ota = args.vendor_channel + "/waydroid_" + \
args.arch + "/" + vendor.replace(" ", "_") + ".json"
vendor_request = helpers.http.retrieve(vendor_ota)
if vendor_request[0] == 200:
args.vendor_type = vendor
args.vendor_ota = vendor_ota
break
if not args.vendor_type:
raise ValueError(
"Failed to get vendor OTA channel: {}".format(vendor_ota))
if args.system_ota != cfg["waydroid"].get("system_ota"):
cfg["waydroid"]["system_datetime"] = tools.config.defaults["system_datetime"]
if args.vendor_ota != cfg["waydroid"].get("vendor_ota"):
cfg["waydroid"]["vendor_datetime"] = tools.config.defaults["vendor_datetime"]
cfg["waydroid"]["vendor_type"] = args.vendor_type
cfg["waydroid"]["system_ota"] = args.system_ota
cfg["waydroid"]["vendor_ota"] = args.vendor_ota
tools.config.save(args, cfg)
return True
def init(args):
if is_initialized(args) and not args.force:
logging.info("Already initialized")
if not setup_config(args):
return
status = "STOPPED"
session = None
if os.path.exists(tools.config.defaults["lxc"] + "/waydroid"):
status = helpers.lxc.status(args)
if status != "STOPPED":
if "running_init_in_service" in args:
session = args.session
tools.actions.container_manager.stop(args, False)
else:
logging.info("Stopping container")
try:
container = tools.helpers.ipc.DBusContainerService()
session = container.GetSession()
container.Stop(False)
except Exception as e:
logging.debug(e)
tools.actions.container_manager.stop(args, False)
if args.images_path not in tools.config.defaults["preinstalled_images_paths"]:
helpers.images.get(args)
else:
helpers.images.remove_overlay(args)
if not os.path.isdir(tools.config.defaults["rootfs"]):
os.mkdir(tools.config.defaults["rootfs"])
if not os.path.isdir(tools.config.defaults["overlay"]):
os.mkdir(tools.config.defaults["overlay"])
os.mkdir(tools.config.defaults["overlay"]+"/vendor")
if not os.path.isdir(tools.config.defaults["overlay_rw"]):
os.mkdir(tools.config.defaults["overlay_rw"])
os.mkdir(tools.config.defaults["overlay_rw"]+"/system")
os.mkdir(tools.config.defaults["overlay_rw"]+"/vendor")
helpers.drivers.probeAshmemDriver(args)
helpers.lxc.setup_host_perms(args)
helpers.lxc.set_lxc_config(args)
helpers.lxc.make_base_props(args)
if status != "STOPPED":
try:
if "running_init_in_service" in args:
tools.actions.container_manager.do_start(args, session)
else:
logging.info("Starting container")
container.Start(session)
except Exception as e:
logging.debug(e)
logging.error("Failed to restart container. Please do so manually.")
class DbusInitializer(dbus.service.Object):
def __init__(self, looper, bus, object_path, args):
self.args = args
self.looper = looper
self.worker_thread = None
dbus.service.Object.__init__(self, bus, object_path)
@helpers.logging.log_exceptions
@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):
if self.worker_thread is not None:
self.worker_thread.kill()
self.worker_thread.join()
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"):
self.worker_thread = remote_init_server(self.args, self, params)
else:
raise PermissionError("Polkit: Authentication failed")
@helpers.logging.log_exceptions
@dbus.service.method("id.waydro.Initializer", in_signature='', out_signature='')
def Cancel(self):
if self.worker_thread is not None:
self.worker_thread.kill()
self.worker_thread.join()
@dbus.service.signal("id.waydro.Initializer", signature='s')
def ProgressChanged(self, message):
pass
@dbus.service.signal("id.waydro.Initializer", signature='')
def Finished(self):
pass
@dbus.service.signal("id.waydro.Initializer", signature='')
def Interrupted(self):
pass
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")
class StdoutRedirect(logging.StreamHandler):
def __init__(self, pipe):
logging.StreamHandler.__init__(self)
self.pipe = pipe
def write(self, s):
self.pipe.send(s)
def flush(self):
pass
def emit(self, record):
if record.levelno >= logging.INFO:
self.write(self.format(record) + self.terminator)
def remote_init_proc_entry(args, pipe):
out = StdoutRedirect(pipe)
sys.stdout = sys.stderr = out
helpers.logging.add_verbose_log_level()
logging.getLogger().addHandler(out)
try:
init(args)
sys.exit(0)
except KeyboardInterrupt:
sys.exit(1)
except Exception:
logging.exception("Exception during init")
sys.exit(1)
finally:
pipe.close()
def remote_init_server(_args, dbus_obj, params):
args = argparse.Namespace()
tools.prep_args(args)
args.details_to_stdout = _args.details_to_stdout
args.verbose = _args.verbose
args.quiet = _args.quiet
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
parent_conn, child_conn = multiprocessing.Pipe(False)
p = multiprocessing.Process(target=remote_init_proc_entry, args=(args, child_conn,), daemon=True)
p.start()
def monitor_init(p, pipe):
try:
while True:
dbus_obj.ProgressChanged(pipe.recv())
except EOFError:
pass
p.join()
if p.exitcode == 0:
GLib.idle_add(dbus_obj.Finished)
else:
GLib.idle_add(dbus_obj.Interrupted)
t = threading.Thread(target=monitor_init, args=(p, parent_conn,), daemon=True)
t.kill = lambda: p.kill()
t.start()
return t
def remote_init_client(args):
# Local imports cause Gtk is intrusive
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
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_name("waydroid")
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")
sysTypesStore = Gtk.ListStore(str, str)
sysTypesStore.append(["VANILLA", "Minimal Android"])
sysTypesStore.append(["GAPPS", "Android with Google Apps"])
sysTypeCombo = Gtk.ComboBox.new_with_model(sysTypesStore)
renderer_text = Gtk.CellRendererText()
sysTypeCombo.pack_start(renderer_text, True)
sysTypeCombo.add_attribute(renderer_text, "text", 1)
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.bus_signals = []
self.initializing = False
self.connect("destroy", self.on_destroy)
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()
sysType = self.sysType.get_model()[self.sysType.get_active_iter()][0]
self.run_init(self.sysOta.get_text(), self.vndOta.get_text(), sysType)
def draw(self, 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 on_progress(self, message):
self.draw(message)
def on_finished(self):
self.initializing = False
if is_initialized(args):
self.doneBtn.show()
self.draw("\nDone\n")
def on_interrupted(self):
self.initializing = False
self.draw("\nInterrupted\n")
def on_reply(self):
self.downloadBtn.set_sensitive(True)
def on_bus_error(self, e):
if e.get_dbus_name() == "org.freedesktop.DBus.Python.PermissionError":
self.draw(e.get_dbus_message().splitlines()[-1] + "\n")
else:
self.draw(str(e))
self.downloadBtn.set_sensitive(True)
def on_destroy(self, _):
if self.initializing:
try:
tools.helpers.ipc.DBusContainerService("/Initializer", "id.waydro.Initializer").Cancel()
except Exception as e:
logging.debug("Unexpected error while cancelling initializer: %s", e)
Gtk.main_quit()
def run_init(self, systemOta, vendorOta, systemType):
for signal in self.bus_signals:
signal.remove()
self.draw("\nWaiting for waydroid container service...\n")
self.bus_signals = []
self.initializing = True
try:
initializer = tools.helpers.ipc.DBusContainerService("/Initializer", "id.waydro.Initializer")
self.bus_signals.append(initializer.connect_to_signal("ProgressChanged", self.on_progress))
self.bus_signals.append(initializer.connect_to_signal("Finished", self.on_finished))
self.bus_signals.append(initializer.connect_to_signal("Interrupted", self.on_interrupted))
params = {
"system_channel": systemOta,
"vendor_channel": vendorOta,
"system_type": systemType
}
initializer.Init(params, reply_handler=self.on_reply, error_handler=self.on_bus_error)
except dbus.DBusException as e:
self.on_bus_error(e)
except Exception as e:
self.draw(f"{e}\n")
GLib.set_prgname("Waydroid")
win = WaydroidInitWindow()
win.show_all()
win.outTextView.hide()
win.doneBtn.hide()
Gtk.main()