]> glassweightruler.freedombox.rocks Git - waydroid.git/blob - tools/actions/initializer.py
packaging: install icons to xdg icons resource path, and get the .desktop file to...
[waydroid.git] / tools / actions / initializer.py
1 # Copyright 2021 Erfan Abdi
2 # SPDX-License-Identifier: GPL-3.0-or-later
3 import logging
4 import os
5 from tools import helpers
6 import tools.config
7
8 import sys
9 import threading
10 import multiprocessing
11 import select
12 import queue
13 import time
14 import dbus
15 import dbus.service
16 from gi.repository import GLib
17
18 def is_initialized(args):
19 return os.path.isfile(args.config) and os.path.isdir(tools.config.defaults["rootfs"])
20
21 def get_vendor_type(args):
22 vndk_str = helpers.props.host_get(args, "ro.vndk.version")
23 ret = "MAINLINE"
24 if vndk_str != "":
25 vndk = int(vndk_str)
26 if vndk > 19:
27 ret = "HALIUM_" + str(vndk - 19)
28
29 return ret
30
31 def setup_config(args):
32 cfg = tools.config.load(args)
33 args.arch = helpers.arch.host()
34 cfg["waydroid"]["arch"] = args.arch
35
36 preinstalled_images_paths = tools.config.defaults["preinstalled_images_paths"]
37 if not args.images_path:
38 for preinstalled_images in preinstalled_images_paths:
39 if os.path.isdir(preinstalled_images):
40 if os.path.isfile(preinstalled_images + "/system.img") and os.path.isfile(preinstalled_images + "/vendor.img"):
41 args.images_path = preinstalled_images
42 break
43 else:
44 logging.warning("Found directory {} but missing system or vendor image, ignoring...".format(preinstalled_images))
45
46 if not args.images_path:
47 args.images_path = tools.config.defaults["images_path"]
48 cfg["waydroid"]["images_path"] = args.images_path
49
50 channels_cfg = tools.config.load_channels()
51 if not args.system_channel:
52 args.system_channel = channels_cfg["channels"]["system_channel"]
53 if not args.vendor_channel:
54 args.vendor_channel = channels_cfg["channels"]["vendor_channel"]
55 if not args.rom_type:
56 args.rom_type = channels_cfg["channels"]["rom_type"]
57 if not args.system_type:
58 args.system_type = channels_cfg["channels"]["system_type"]
59
60 args.system_ota = args.system_channel + "/" + args.rom_type + \
61 "/waydroid_" + args.arch + "/" + args.system_type + ".json"
62 system_request = helpers.http.retrieve(args.system_ota)
63 if system_request[0] != 200:
64 if args.images_path not in preinstalled_images_paths:
65 raise ValueError(
66 "Failed to get system OTA channel: {}, error: {}".format(args.system_ota, system_request[0]))
67 else:
68 args.system_ota = "None"
69
70 device_codename = helpers.props.host_get(args, "ro.product.device")
71 args.vendor_type = None
72 for vendor in [device_codename, get_vendor_type(args)]:
73 vendor_ota = args.vendor_channel + "/waydroid_" + \
74 args.arch + "/" + vendor.replace(" ", "_") + ".json"
75 vendor_request = helpers.http.retrieve(vendor_ota)
76 if vendor_request[0] == 200:
77 args.vendor_type = vendor
78 args.vendor_ota = vendor_ota
79 break
80
81 if not args.vendor_type:
82 if args.images_path not in preinstalled_images_paths:
83 raise ValueError(
84 "Failed to get vendor OTA channel: {}".format(vendor_ota))
85 else:
86 args.vendor_ota = "None"
87 args.vendor_type = get_vendor_type(args)
88
89 if args.system_ota != cfg["waydroid"].get("system_ota"):
90 cfg["waydroid"]["system_datetime"] = tools.config.defaults["system_datetime"]
91 if args.vendor_ota != cfg["waydroid"].get("vendor_ota"):
92 cfg["waydroid"]["vendor_datetime"] = tools.config.defaults["vendor_datetime"]
93
94 cfg["waydroid"]["vendor_type"] = args.vendor_type
95 cfg["waydroid"]["system_ota"] = args.system_ota
96 cfg["waydroid"]["vendor_ota"] = args.vendor_ota
97 helpers.drivers.setupBinderNodes(args)
98 cfg["waydroid"]["binder"] = args.BINDER_DRIVER
99 cfg["waydroid"]["vndbinder"] = args.VNDBINDER_DRIVER
100 cfg["waydroid"]["hwbinder"] = args.HWBINDER_DRIVER
101 tools.config.save(args, cfg)
102
103 def init(args):
104 if not is_initialized(args) or args.force:
105 initializer_service = None
106 try:
107 initializer_service = tools.helpers.ipc.DBusContainerService("/Initializer", "id.waydro.Initializer")
108 except dbus.DBusException:
109 pass
110 setup_config(args)
111 status = "STOPPED"
112 if os.path.exists(tools.config.defaults["lxc"] + "/waydroid"):
113 status = helpers.lxc.status(args)
114 if status != "STOPPED":
115 logging.info("Stopping container")
116 helpers.lxc.stop(args)
117 helpers.images.umount_rootfs(args)
118 if args.images_path not in tools.config.defaults["preinstalled_images_paths"]:
119 helpers.images.get(args)
120 else:
121 helpers.images.remove_overlay(args)
122 if not os.path.isdir(tools.config.defaults["rootfs"]):
123 os.mkdir(tools.config.defaults["rootfs"])
124 if not os.path.isdir(tools.config.defaults["overlay"]):
125 os.mkdir(tools.config.defaults["overlay"])
126 os.mkdir(tools.config.defaults["overlay"]+"/vendor")
127 if not os.path.isdir(tools.config.defaults["overlay_rw"]):
128 os.mkdir(tools.config.defaults["overlay_rw"])
129 os.mkdir(tools.config.defaults["overlay_rw"]+"/system")
130 os.mkdir(tools.config.defaults["overlay_rw"]+"/vendor")
131 helpers.drivers.probeAshmemDriver(args)
132 helpers.lxc.setup_host_perms(args)
133 helpers.lxc.set_lxc_config(args)
134 helpers.lxc.make_base_props(args)
135 if status != "STOPPED":
136 logging.info("Starting container")
137 helpers.images.mount_rootfs(args, args.images_path)
138 helpers.lxc.start(args)
139
140 if "running_init_in_service" not in args or not args.running_init_in_service:
141 try:
142 if initializer_service:
143 initializer_service.Done()
144 except dbus.DBusException:
145 pass
146 else:
147 logging.info("Already initialized")
148
149 def wait_for_init(args):
150 helpers.ipc.create_channel("remote_init_output")
151
152 mainloop = GLib.MainLoop()
153 dbus_obj = DbusInitializer(mainloop, dbus.SystemBus(), '/Initializer', args)
154 mainloop.run()
155
156 # After init
157 dbus_obj.remove_from_connection()
158
159 class DbusInitializer(dbus.service.Object):
160 def __init__(self, looper, bus, object_path, args):
161 self.args = args
162 self.looper = looper
163 dbus.service.Object.__init__(self, bus, object_path)
164
165 @dbus.service.method("id.waydro.Initializer", in_signature='a{ss}', out_signature='', sender_keyword="sender", connection_keyword="conn")
166 def Init(self, params, sender=None, conn=None):
167 channels_cfg = tools.config.load_channels()
168 no_auth = params["system_channel"] == channels_cfg["channels"]["system_channel"] and \
169 params["vendor_channel"] == channels_cfg["channels"]["vendor_channel"]
170 if no_auth or ensure_polkit_auth(sender, conn, "id.waydro.Initializer.Init"):
171 threading.Thread(target=remote_init_server, args=(self.args, params)).start()
172 else:
173 raise PermissionError("Polkit: Authentication failed")
174
175 @dbus.service.method("id.waydro.Initializer", in_signature='', out_signature='')
176 def Done(self):
177 if is_initialized(self.args):
178 self.looper.quit()
179
180 def ensure_polkit_auth(sender, conn, privilege):
181 dbus_info = dbus.Interface(conn.get_object("org.freedesktop.DBus", "/org/freedesktop/DBus/Bus", False), "org.freedesktop.DBus")
182 pid = dbus_info.GetConnectionUnixProcessID(sender)
183 polkit = dbus.Interface(dbus.SystemBus().get_object("org.freedesktop.PolicyKit1", "/org/freedesktop/PolicyKit1/Authority", False), "org.freedesktop.PolicyKit1.Authority")
184 try:
185 (is_auth, _, _) = polkit.CheckAuthorization(
186 ("unix-process", {
187 "pid": dbus.UInt32(pid, variant_level=1),
188 "start-time": dbus.UInt64(0, variant_level=1)}),
189 privilege, {"AllowUserInteraction": "true"},
190 dbus.UInt32(1),
191 "",
192 timeout=300)
193 return is_auth
194 except dbus.DBusException:
195 raise PermissionError("Polkit: Authentication timed out")
196
197 def background_remote_init_process(args):
198 with helpers.ipc.open_channel("remote_init_output", "wb") as channel_out:
199 class StdoutRedirect(logging.StreamHandler):
200 def write(self, s):
201 channel_out.write(str.encode(s))
202 def flush(self):
203 pass
204 def emit(self, record):
205 if record.levelno >= logging.INFO:
206 self.write(self.format(record) + self.terminator)
207
208 out = StdoutRedirect()
209 sys.stdout = sys.stderr = out
210 logging.getLogger().addHandler(out)
211
212 ctl_queue = queue.Queue()
213 def try_init(args):
214 try:
215 init(args)
216 except Exception as e:
217 print(str(e))
218 finally:
219 ctl_queue.put(0)
220
221 def poll_pipe():
222 poller = select.poll()
223 poller.register(channel_out, select.POLLERR)
224 poller.poll()
225 # When reaching here the client was terminated
226 ctl_queue.put(0)
227
228 init_thread = threading.Thread(target=try_init, args=(args,))
229 init_thread.daemon = True
230 init_thread.start()
231
232 poll_thread = threading.Thread(target=poll_pipe)
233 poll_thread.daemon = True
234 poll_thread.start()
235
236 # Join any one of the two threads
237 # Then exit the subprocess to kill the remaining thread.
238 # Can you believe this is the only way to kill a thread in python???
239 ctl_queue.get()
240
241 sys.stdout = sys.__stdout__
242 sys.stderr = sys.__stderr__
243 logging.getLogger().removeHandler(out)
244
245 def remote_init_server(args, params):
246 args.force = True
247 args.images_path = ""
248 args.rom_type = ""
249 args.system_channel = params["system_channel"]
250 args.vendor_channel = params["vendor_channel"]
251 args.system_type = params["system_type"]
252 args.running_init_in_service = True
253
254 p = multiprocessing.Process(target=background_remote_init_process, args=(args,))
255 p.daemon = True
256 p.start()
257 p.join()
258
259 def remote_init_client(args):
260 # Local imports cause Gtk is intrusive
261 import gi
262 gi.require_version("Gtk", "3.0")
263 from gi.repository import Gtk
264
265 bus = dbus.SystemBus()
266
267 if is_initialized(args):
268 try:
269 tools.helpers.ipc.DBusContainerService("/Initializer", "id.waydro.Initializer").Done()
270 except dbus.DBusException:
271 pass
272 return
273
274 def notify_and_quit(caller):
275 if is_initialized(args):
276 try:
277 tools.helpers.ipc.DBusContainerService("/Initializer", "id.waydro.Initializer").Done()
278 except dbus.DBusException:
279 pass
280 GLib.idle_add(Gtk.main_quit)
281
282 class WaydroidInitWindow(Gtk.Window):
283 def __init__(self):
284 super().__init__(title="Initialize Waydroid")
285 channels_cfg = tools.config.load_channels()
286
287 self.set_default_size(600, 250)
288 self.set_icon_from_file(tools.config.tools_src + "/data/AppIcon.png")
289
290 grid = Gtk.Grid(row_spacing=6, column_spacing=6, margin=10, column_homogeneous=True)
291 grid.set_hexpand(True)
292 grid.set_vexpand(True)
293 self.add(grid)
294
295 sysOtaLabel = Gtk.Label("System OTA")
296 sysOtaEntry = Gtk.Entry()
297 sysOtaEntry.set_text(channels_cfg["channels"]["system_channel"])
298 grid.attach(sysOtaLabel, 0, 0, 1, 1)
299 grid.attach_next_to(sysOtaEntry ,sysOtaLabel, Gtk.PositionType.RIGHT, 2, 1)
300 self.sysOta = sysOtaEntry.get_buffer()
301
302 vndOtaLabel = Gtk.Label("Vendor OTA")
303 vndOtaEntry = Gtk.Entry()
304 vndOtaEntry.set_text(channels_cfg["channels"]["vendor_channel"])
305 grid.attach(vndOtaLabel, 0, 1, 1, 1)
306 grid.attach_next_to(vndOtaEntry, vndOtaLabel, Gtk.PositionType.RIGHT, 2, 1)
307 self.vndOta = vndOtaEntry.get_buffer()
308
309 sysTypeLabel = Gtk.Label("Android Type")
310 sysTypeCombo = Gtk.ComboBoxText()
311 sysTypeCombo.set_entry_text_column(0)
312 for t in ["VANILLA", "GAPPS"]:
313 sysTypeCombo.append_text(t)
314 sysTypeCombo.set_active(0)
315 grid.attach(sysTypeLabel, 0, 2, 1, 1)
316 grid.attach_next_to(sysTypeCombo, sysTypeLabel, Gtk.PositionType.RIGHT, 2, 1)
317 self.sysType = sysTypeCombo
318
319 downloadBtn = Gtk.Button("Download")
320 downloadBtn.connect("clicked", self.on_download_btn_clicked)
321 grid.attach(downloadBtn, 1,3,1,1)
322 self.downloadBtn = downloadBtn
323
324 doneBtn = Gtk.Button("Done")
325 doneBtn.connect("clicked", lambda x: self.destroy())
326 doneBtn.get_style_context().add_class('suggested-action')
327 grid.attach_next_to(doneBtn, downloadBtn, Gtk.PositionType.RIGHT, 1, 1)
328 self.doneBtn = doneBtn
329
330 outScrolledWindow = Gtk.ScrolledWindow()
331 outScrolledWindow.set_hexpand(True)
332 outScrolledWindow.set_vexpand(True)
333 outTextView = Gtk.TextView()
334 outTextView.set_property('editable', False)
335 outTextView.set_property('cursor-visible', False)
336 outScrolledWindow.add(outTextView)
337 grid.attach(outScrolledWindow, 0, 4, 3, 1)
338 self.outScrolledWindow = outScrolledWindow
339 self.outTextView = outTextView
340 self.outBuffer = outTextView.get_buffer()
341 self.outBuffer.create_mark("end", self.outBuffer.get_end_iter(), False)
342
343 self.open_channel = None
344
345 def scroll_to_bottom(self):
346 self.outTextView.scroll_mark_onscreen(self.outBuffer.get_mark("end"))
347
348 def on_download_btn_clicked(self, widget):
349 widget.set_sensitive(False)
350 self.doneBtn.hide()
351 self.outTextView.show()
352 init_params = (self.sysOta.get_text(), self.vndOta.get_text(), self.sysType.get_active_text())
353 init_runner = threading.Thread(target=self.run_init, args=init_params)
354 init_runner.daemon = True
355 init_runner.start()
356
357 def run_init(self, systemOta, vendorOta, systemType):
358 def draw_sync(s):
359 if s.startswith('\r'):
360 last = self.outBuffer.get_iter_at_line(self.outBuffer.get_line_count()-1)
361 last.backward_char()
362 self.outBuffer.delete(last, self.outBuffer.get_end_iter())
363 self.outBuffer.insert(self.outBuffer.get_end_iter(), s)
364 self.scroll_to_bottom()
365 def draw(s):
366 GLib.idle_add(draw_sync, s)
367
368 if self.open_channel is not None:
369 self.open_channel.close()
370 # Wait for other end to reset
371 time.sleep(1)
372
373 draw("Waiting for waydroid container service...\n")
374 try:
375 params = {
376 "system_channel": self.sysOta.get_text(),
377 "vendor_channel": self.vndOta.get_text(),
378 "system_type": self.sysType.get_active_text()
379 }
380 tools.helpers.ipc.DBusContainerService("/Initializer", "id.waydro.Initializer").Init(params, timeout=310)
381 except dbus.DBusException as e:
382 if e.get_dbus_name() == "org.freedesktop.DBus.Python.PermissionError":
383 draw(e.get_dbus_message().splitlines()[-1] + "\n")
384 else:
385 draw("The waydroid container service is not listening\n")
386 GLib.idle_add(self.downloadBtn.set_sensitive, True)
387 return
388
389 with helpers.ipc.open_channel("remote_init_output", "rb") as channel:
390 self.open_channel = channel
391 GLib.idle_add(self.downloadBtn.set_sensitive, True)
392 line = ""
393 try:
394 while True:
395 data = channel.read(1)
396 if len(data) == 0:
397 draw(line)
398 break
399 c = data.decode()
400 if c == '\r':
401 draw(line)
402 line = c
403 else:
404 line += c
405 if c == '\n':
406 draw(line)
407 line = ""
408 except:
409 draw("\nInterrupted\n")
410
411 if is_initialized(args):
412 GLib.idle_add(self.doneBtn.show)
413 draw("Done\n")
414
415
416 GLib.set_prgname("Waydroid")
417 win = WaydroidInitWindow()
418 win.connect("destroy", notify_and_quit)
419
420 win.show_all()
421 win.outTextView.hide()
422 win.doneBtn.hide()
423
424 Gtk.main()