1 # Copyright 2021 Erfan Abdi
2 # SPDX-License-Identifier: GPL-3.0-or-later
5 from tools
import helpers
10 import multiprocessing
16 from gi
.repository
import GLib
18 def is_initialized(args
):
19 return os
.path
.isfile(args
.config
) and os
.path
.isdir(tools
.config
.defaults
["rootfs"])
21 def get_vendor_type(args
):
22 vndk_str
= helpers
.props
.host_get(args
, "ro.vndk.version")
27 halium_ver
= vndk
- 19
29 halium_ver
-= 1 # 12L -> Halium 12
30 ret
= "HALIUM_" + str(halium_ver
)
36 def setup_config(args
):
37 cfg
= tools
.config
.load(args
)
38 args
.arch
= helpers
.arch
.host()
39 cfg
["waydroid"]["arch"] = args
.arch
41 args
.vendor_type
= get_vendor_type(args
)
42 cfg
["waydroid"]["vendor_type"] = args
.vendor_type
44 helpers
.drivers
.setupBinderNodes(args
)
45 cfg
["waydroid"]["binder"] = args
.BINDER_DRIVER
46 cfg
["waydroid"]["vndbinder"] = args
.VNDBINDER_DRIVER
47 cfg
["waydroid"]["hwbinder"] = args
.HWBINDER_DRIVER
49 has_preinstalled_images
= False
50 preinstalled_images_paths
= tools
.config
.defaults
["preinstalled_images_paths"]
51 for preinstalled_images
in preinstalled_images_paths
:
52 if os
.path
.isdir(preinstalled_images
):
53 if os
.path
.isfile(preinstalled_images
+ "/system.img") and os
.path
.isfile(preinstalled_images
+ "/vendor.img"):
54 has_preinstalled_images
= True
55 args
.images_path
= preinstalled_images
58 logging
.warning("Found directory {} but missing system or vendor image, ignoring...".format(preinstalled_images
))
60 if not args
.images_path
:
61 args
.images_path
= tools
.config
.defaults
["images_path"]
62 cfg
["waydroid"]["images_path"] = args
.images_path
64 if has_preinstalled_images
:
65 cfg
["waydroid"]["system_ota"] = args
.system_ota
= "None"
66 cfg
["waydroid"]["vendor_ota"] = args
.vendor_ota
= "None"
67 cfg
["waydroid"]["system_datetime"] = tools
.config
.defaults
["system_datetime"]
68 cfg
["waydroid"]["vendor_datetime"] = tools
.config
.defaults
["vendor_datetime"]
69 tools
.config
.save(args
, cfg
)
72 channels_cfg
= tools
.config
.load_channels()
73 if not args
.system_channel
:
74 args
.system_channel
= channels_cfg
["channels"]["system_channel"]
75 if not args
.vendor_channel
:
76 args
.vendor_channel
= channels_cfg
["channels"]["vendor_channel"]
78 args
.rom_type
= channels_cfg
["channels"]["rom_type"]
79 if not args
.system_type
:
80 args
.system_type
= channels_cfg
["channels"]["system_type"]
82 if not args
.system_channel
or not args
.vendor_channel
:
83 logging
.error("ERROR: You must provide 'System OTA' and 'Vendor OTA' URLs.")
86 args
.system_ota
= args
.system_channel
+ "/" + args
.rom_type
+ \
87 "/waydroid_" + args
.arch
+ "/" + args
.system_type
+ ".json"
88 system_request
= helpers
.http
.retrieve(args
.system_ota
)
89 if system_request
[0] != 200:
91 "Failed to get system OTA channel: {}, error: {}".format(args
.system_ota
, system_request
[0]))
93 device_codename
= helpers
.props
.host_get(args
, "ro.product.device")
94 args
.vendor_type
= None
95 for vendor
in [device_codename
, get_vendor_type(args
)]:
96 vendor_ota
= args
.vendor_channel
+ "/waydroid_" + \
97 args
.arch
+ "/" + vendor
.replace(" ", "_") + ".json"
98 vendor_request
= helpers
.http
.retrieve(vendor_ota
)
99 if vendor_request
[0] == 200:
100 args
.vendor_type
= vendor
101 args
.vendor_ota
= vendor_ota
104 if not args
.vendor_type
:
106 "Failed to get vendor OTA channel: {}".format(vendor_ota
))
108 if args
.system_ota
!= cfg
["waydroid"].get("system_ota"):
109 cfg
["waydroid"]["system_datetime"] = tools
.config
.defaults
["system_datetime"]
110 if args
.vendor_ota
!= cfg
["waydroid"].get("vendor_ota"):
111 cfg
["waydroid"]["vendor_datetime"] = tools
.config
.defaults
["vendor_datetime"]
113 cfg
["waydroid"]["vendor_type"] = args
.vendor_type
114 cfg
["waydroid"]["system_ota"] = args
.system_ota
115 cfg
["waydroid"]["vendor_ota"] = args
.vendor_ota
116 tools
.config
.save(args
, cfg
)
120 if not is_initialized(args
) or args
.force
:
121 initializer_service
= None
123 initializer_service
= tools
.helpers
.ipc
.DBusContainerService("/Initializer", "id.waydro.Initializer")
124 except dbus
.DBusException
:
126 if not setup_config(args
):
129 if os
.path
.exists(tools
.config
.defaults
["lxc"] + "/waydroid"):
130 status
= helpers
.lxc
.status(args
)
131 if status
!= "STOPPED":
132 logging
.info("Stopping container")
134 container
= tools
.helpers
.ipc
.DBusContainerService()
135 args
.session
= container
.GetSession()
136 container
.Stop(False)
137 except Exception as e
:
139 tools
.actions
.container_manager
.stop(args
)
140 if args
.images_path
not in tools
.config
.defaults
["preinstalled_images_paths"]:
141 helpers
.images
.get(args
)
143 helpers
.images
.remove_overlay(args
)
144 if not os
.path
.isdir(tools
.config
.defaults
["rootfs"]):
145 os
.mkdir(tools
.config
.defaults
["rootfs"])
146 if not os
.path
.isdir(tools
.config
.defaults
["overlay"]):
147 os
.mkdir(tools
.config
.defaults
["overlay"])
148 os
.mkdir(tools
.config
.defaults
["overlay"]+"/vendor")
149 if not os
.path
.isdir(tools
.config
.defaults
["overlay_rw"]):
150 os
.mkdir(tools
.config
.defaults
["overlay_rw"])
151 os
.mkdir(tools
.config
.defaults
["overlay_rw"]+"/system")
152 os
.mkdir(tools
.config
.defaults
["overlay_rw"]+"/vendor")
153 helpers
.drivers
.probeAshmemDriver(args
)
154 helpers
.lxc
.setup_host_perms(args
)
155 helpers
.lxc
.set_lxc_config(args
)
156 helpers
.lxc
.make_base_props(args
)
157 if status
!= "STOPPED":
158 logging
.info("Starting container")
160 container
.Start(args
.session
)
161 except Exception as e
:
163 logging
.error("Failed to restart container. Please do so manually.")
165 if "running_init_in_service" not in args
or not args
.running_init_in_service
:
167 if initializer_service
:
168 initializer_service
.Done()
169 except dbus
.DBusException
:
172 logging
.info("Already initialized")
174 def wait_for_init(args
):
175 helpers
.ipc
.create_channel("remote_init_output")
177 mainloop
= GLib
.MainLoop()
178 dbus_obj
= DbusInitializer(mainloop
, dbus
.SystemBus(), '/Initializer', args
)
182 dbus_obj
.remove_from_connection()
184 class DbusInitializer(dbus
.service
.Object
):
185 def __init__(self
, looper
, bus
, object_path
, args
):
188 dbus
.service
.Object
.__init
__(self
, bus
, object_path
)
190 @dbus.service.method("id.waydro.Initializer", in_signature
='a{ss}', out_signature
='', sender_keyword
="sender", connection_keyword
="conn")
191 def Init(self
, params
, sender
=None, conn
=None):
192 channels_cfg
= tools
.config
.load_channels()
193 no_auth
= params
["system_channel"] == channels_cfg
["channels"]["system_channel"] and \
194 params
["vendor_channel"] == channels_cfg
["channels"]["vendor_channel"]
195 if no_auth
or ensure_polkit_auth(sender
, conn
, "id.waydro.Initializer.Init"):
196 threading
.Thread(target
=remote_init_server
, args
=(self
.args
, params
)).start()
198 raise PermissionError("Polkit: Authentication failed")
200 @dbus.service.method("id.waydro.Initializer", in_signature
='', out_signature
='')
202 if is_initialized(self
.args
):
205 def ensure_polkit_auth(sender
, conn
, privilege
):
206 dbus_info
= dbus
.Interface(conn
.get_object("org.freedesktop.DBus", "/org/freedesktop/DBus/Bus", False), "org.freedesktop.DBus")
207 pid
= dbus_info
.GetConnectionUnixProcessID(sender
)
208 polkit
= dbus
.Interface(dbus
.SystemBus().get_object("org.freedesktop.PolicyKit1", "/org/freedesktop/PolicyKit1/Authority", False), "org.freedesktop.PolicyKit1.Authority")
210 (is_auth
, _
, _
) = polkit
.CheckAuthorization(
212 "pid": dbus
.UInt32(pid
, variant_level
=1),
213 "start-time": dbus
.UInt64(0, variant_level
=1)}),
214 privilege
, {"AllowUserInteraction": "true"}
,
219 except dbus
.DBusException
:
220 raise PermissionError("Polkit: Authentication timed out")
222 def background_remote_init_process(args
):
223 with helpers
.ipc
.open_channel("remote_init_output", "wb") as channel_out
:
224 class StdoutRedirect(logging
.StreamHandler
):
226 channel_out
.write(str.encode(s
))
229 def emit(self
, record
):
230 if record
.levelno
>= logging
.INFO
:
231 self
.write(self
.format(record
) + self
.terminator
)
233 out
= StdoutRedirect()
234 sys
.stdout
= sys
.stderr
= out
235 logging
.getLogger().addHandler(out
)
237 ctl_queue
= queue
.Queue()
241 except Exception as e
:
247 poller
= select
.poll()
248 poller
.register(channel_out
, select
.POLLERR
)
250 # When reaching here the client was terminated
253 init_thread
= threading
.Thread(target
=try_init
, args
=(args
,))
254 init_thread
.daemon
= True
257 poll_thread
= threading
.Thread(target
=poll_pipe
)
258 poll_thread
.daemon
= True
261 # Join any one of the two threads
262 # Then exit the subprocess to kill the remaining thread.
263 # Can you believe this is the only way to kill a thread in python???
266 sys
.stdout
= sys
.__stdout
__
267 sys
.stderr
= sys
.__stderr
__
268 logging
.getLogger().removeHandler(out
)
270 def remote_init_server(args
, params
):
272 args
.images_path
= ""
274 args
.system_channel
= params
["system_channel"]
275 args
.vendor_channel
= params
["vendor_channel"]
276 args
.system_type
= params
["system_type"]
277 args
.running_init_in_service
= True
279 p
= multiprocessing
.Process(target
=background_remote_init_process
, args
=(args
,))
284 def remote_init_client(args
):
285 # Local imports cause Gtk is intrusive
287 gi
.require_version("Gtk", "3.0")
288 from gi
.repository
import Gtk
290 bus
= dbus
.SystemBus()
292 if is_initialized(args
):
294 tools
.helpers
.ipc
.DBusContainerService("/Initializer", "id.waydro.Initializer").Done()
295 except dbus
.DBusException
:
299 def notify_and_quit(caller
):
300 if is_initialized(args
):
302 tools
.helpers
.ipc
.DBusContainerService("/Initializer", "id.waydro.Initializer").Done()
303 except dbus
.DBusException
:
305 GLib
.idle_add(Gtk
.main_quit
)
307 class WaydroidInitWindow(Gtk
.Window
):
309 super().__init
__(title
="Initialize Waydroid")
310 channels_cfg
= tools
.config
.load_channels()
312 self
.set_default_size(600, 250)
313 self
.set_icon_name("waydroid")
315 grid
= Gtk
.Grid(row_spacing
=6, column_spacing
=6, margin
=10, column_homogeneous
=True)
316 grid
.set_hexpand(True)
317 grid
.set_vexpand(True)
320 sysOtaLabel
= Gtk
.Label("System OTA")
321 sysOtaEntry
= Gtk
.Entry()
322 sysOtaEntry
.set_text(channels_cfg
["channels"]["system_channel"])
323 grid
.attach(sysOtaLabel
, 0, 0, 1, 1)
324 grid
.attach_next_to(sysOtaEntry
,sysOtaLabel
, Gtk
.PositionType
.RIGHT
, 2, 1)
325 self
.sysOta
= sysOtaEntry
.get_buffer()
327 vndOtaLabel
= Gtk
.Label("Vendor OTA")
328 vndOtaEntry
= Gtk
.Entry()
329 vndOtaEntry
.set_text(channels_cfg
["channels"]["vendor_channel"])
330 grid
.attach(vndOtaLabel
, 0, 1, 1, 1)
331 grid
.attach_next_to(vndOtaEntry
, vndOtaLabel
, Gtk
.PositionType
.RIGHT
, 2, 1)
332 self
.vndOta
= vndOtaEntry
.get_buffer()
334 sysTypeLabel
= Gtk
.Label("Android Type")
335 sysTypeCombo
= Gtk
.ComboBoxText()
336 sysTypeCombo
.set_entry_text_column(0)
337 for t
in ["VANILLA", "GAPPS"]:
338 sysTypeCombo
.append_text(t
)
339 sysTypeCombo
.set_active(0)
340 grid
.attach(sysTypeLabel
, 0, 2, 1, 1)
341 grid
.attach_next_to(sysTypeCombo
, sysTypeLabel
, Gtk
.PositionType
.RIGHT
, 2, 1)
342 self
.sysType
= sysTypeCombo
344 downloadBtn
= Gtk
.Button("Download")
345 downloadBtn
.connect("clicked", self
.on_download_btn_clicked
)
346 grid
.attach(downloadBtn
, 1,3,1,1)
347 self
.downloadBtn
= downloadBtn
349 doneBtn
= Gtk
.Button("Done")
350 doneBtn
.connect("clicked", lambda x
: self
.destroy())
351 doneBtn
.get_style_context().add_class('suggested-action')
352 grid
.attach_next_to(doneBtn
, downloadBtn
, Gtk
.PositionType
.RIGHT
, 1, 1)
353 self
.doneBtn
= doneBtn
355 outScrolledWindow
= Gtk
.ScrolledWindow()
356 outScrolledWindow
.set_hexpand(True)
357 outScrolledWindow
.set_vexpand(True)
358 outTextView
= Gtk
.TextView()
359 outTextView
.set_property('editable', False)
360 outTextView
.set_property('cursor-visible', False)
361 outScrolledWindow
.add(outTextView
)
362 grid
.attach(outScrolledWindow
, 0, 4, 3, 1)
363 self
.outScrolledWindow
= outScrolledWindow
364 self
.outTextView
= outTextView
365 self
.outBuffer
= outTextView
.get_buffer()
366 self
.outBuffer
.create_mark("end", self
.outBuffer
.get_end_iter(), False)
368 self
.open_channel
= None
370 def scroll_to_bottom(self
):
371 self
.outTextView
.scroll_mark_onscreen(self
.outBuffer
.get_mark("end"))
373 def on_download_btn_clicked(self
, widget
):
374 widget
.set_sensitive(False)
376 self
.outTextView
.show()
377 init_params
= (self
.sysOta
.get_text(), self
.vndOta
.get_text(), self
.sysType
.get_active_text())
378 init_runner
= threading
.Thread(target
=self
.run_init
, args
=init_params
)
379 init_runner
.daemon
= True
382 def run_init(self
, systemOta
, vendorOta
, systemType
):
384 if s
.startswith('\r'):
385 last
= self
.outBuffer
.get_iter_at_line(self
.outBuffer
.get_line_count()-1)
387 self
.outBuffer
.delete(last
, self
.outBuffer
.get_end_iter())
388 self
.outBuffer
.insert(self
.outBuffer
.get_end_iter(), s
)
389 self
.scroll_to_bottom()
391 GLib
.idle_add(draw_sync
, s
)
393 if self
.open_channel
is not None:
394 self
.open_channel
.close()
395 # Wait for other end to reset
398 draw("Waiting for waydroid container service...\n")
401 "system_channel": self
.sysOta
.get_text(),
402 "vendor_channel": self
.vndOta
.get_text(),
403 "system_type": self
.sysType
.get_active_text()
405 tools
.helpers
.ipc
.DBusContainerService("/Initializer", "id.waydro.Initializer").Init(params
, timeout
=310)
406 except dbus
.DBusException
as e
:
407 if e
.get_dbus_name() == "org.freedesktop.DBus.Python.PermissionError":
408 draw(e
.get_dbus_message().splitlines()[-1] + "\n")
410 draw("The waydroid container service is not listening\n")
411 GLib
.idle_add(self
.downloadBtn
.set_sensitive
, True)
414 with helpers
.ipc
.open_channel("remote_init_output", "rb") as channel
:
415 self
.open_channel
= channel
416 GLib
.idle_add(self
.downloadBtn
.set_sensitive
, True)
420 data
= channel
.read(1)
434 draw("\nInterrupted\n")
436 if is_initialized(args
):
437 GLib
.idle_add(self
.doneBtn
.show
)
441 GLib
.set_prgname("Waydroid")
442 win
= WaydroidInitWindow()
443 win
.connect("destroy", notify_and_quit
)
446 win
.outTextView
.hide()