Skip to content

Commit

Permalink
Start Firefox before Anaconda on Live
Browse files Browse the repository at this point in the history
The execution time between Gnome Initial Setup and Anaconda is quite big
we need to reduce it by moving FF execution sooner before Anaconda is
executed.

Time to show Firefox window based on my measurements on VM with 2GB RAM
and 4vCPUs:
Original execution time: 13 seconds
With this commit: less than 6 seconds

If we want to reduce this time even more we need to do that in Firefox
or Gnome Shell (Gnome Initial Setup environment) somehow.

Resolves: rhbz#2236438
  • Loading branch information
jkonecny12 committed Sep 12, 2023
1 parent 86fb8a9 commit 5b56da9
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 14 deletions.
14 changes: 12 additions & 2 deletions data/liveinst/liveinst
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,23 @@ if [ -n "$UPDATES" ]; then

fi

start_anaconda() {
# early execution of Firefox to reduce starting time
mkdir -p /run/anaconda
/usr/libexec/webui-desktop -t live /cockpit/@localhost/anaconda-webui/index.html &
echo "$!" > /run/anaconda/webui_script.pid

# start Anaconda main process
$ANACONDA "$@"
}

# Force the X11 backend since sudo and wayland do not mix
export GDK_BACKEND=x11

if [ -x /usr/bin/udisks ]; then
/usr/bin/udisks --inhibit -- "$ANACONDA" "$@"
/usr/bin/udisks --inhibit -- start_anaconda "$@"
else
$ANACONDA "$@"
start_anaconda "$@"
fi

if [ -e /tmp/updates.img ]; then rm /tmp/updates.img; fi
Expand Down
3 changes: 3 additions & 0 deletions pyanaconda/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@
ANACONDA_CONFIG_DIR = "/etc/anaconda/"
ANACONDA_CONFIG_TMP = "/run/anaconda/anaconda.conf"

# file to store pid of the web viewer app to show Anaconda locally
WEBUI_VIEWER_PID_FILE = "/run/anaconda/webui_script.pid"

# NOTE: this should be LANG_TERRITORY.CODESET, e.g. en_US.UTF-8
DEFAULT_LANG = "en_US.UTF-8"

Expand Down
62 changes: 50 additions & 12 deletions pyanaconda/ui/webui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
import meh

from pyanaconda import ui
from pyanaconda.core.constants import QUIT_MESSAGE, PAYLOAD_TYPE_DNF
from pyanaconda.core.constants import QUIT_MESSAGE, PAYLOAD_TYPE_DNF, WEBUI_VIEWER_PID_FILE
from pyanaconda.core.util import startProgram
from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.core.threads import thread_manager
from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.core.process_watchers import PidWatcher
from pyanaconda.core.glib import create_main_loop

log = get_module_logger(__name__)

Expand Down Expand Up @@ -65,6 +67,8 @@ def __init__(self, storage, payload, remote,
self.remote = remote
self.quitMessage = quitMessage
self._meh_interface = meh.ui.text.TextIntf()
self._main_loop = None
self._viewer_pid_file = WEBUI_VIEWER_PID_FILE

def setup(self, data):
"""Construct all the objects required to implement this interface.
Expand All @@ -90,25 +94,59 @@ def run(self):
"""Run the interface."""
log.debug("web-ui: starting cockpit web view")

# Force Firefox to be used via the BROWSER environment variable.
# This is read by cockpit-desktop and makes it launch Firefox in kiosk mode
# instead of the GTK WebKit based web view it launches by default.

# FIXME: looks like "type" should not be used and _is_live_os is private ?
if conf.system.provides_liveuser:
profile_name = FIREFOX_THEME_LIVE
self._watch_webui_on_live()
else:
profile_name = FIREFOX_THEME_DEFAULT
self._run_webui()

def _run_webui(self):
# FIXME: This part should be start event loop (could use the WatchProcesses class)
# FIXME: We probably want to move this to early execution similar to what we have on live
profile_name = FIREFOX_THEME_DEFAULT

proc = startProgram(["/usr/libexec/webui-desktop",
"-t", profile_name, "-r", str(int(self.remote)),
"/cockpit/@localhost/anaconda-webui/index.html"],
"-t", profile_name, "-r", str(int(self.remote)),
"/cockpit/@localhost/anaconda-webui/index.html"],
reset_lang=False)
log.debug("cockpit web view has been started")
with open("/run/anaconda/webui_script.pid", "w") as f:
with open(self._viewer_pid_file, "w") as f:
f.write(repr(proc.pid))
proc.wait()
log.debug("cockpit web view has finished running")

def _watch_webui_on_live(self):
"""Watch webui-desktop script process on Live.
It takes long time to start Firefox after the user interaction (on live even 20+ seconds).
To avoid that, we are starting the webui-desktop script early in the liveinst script.
Here we are just watching if the process is still running. If the browser is closed
Anaconda main process will stop.
"""
log.debug("web-ui: watching webui-desktop pid on live environment")
pid = -1

try:
with open(self._viewer_pid_file, "tr") as f:
pid = int(f.readline().strip())
except ValueError:
raise ValueError("Anaconda can't obtain pid of the web UI viewer application")

if pid < 0:
raise ValueError("Anaconda web UI viewer pid file seems to be broken")

PidWatcher().watch_process(pid, self._webui_desktop_closed)

self._main_loop = create_main_loop()
self._main_loop.run()

log.debug("web-ui: cockpit web view has finished running")

def _webui_desktop_closed(self, pid, status):
if status != 0:
log.warning("web-ui: the webui-desktop script ended abruptly!")

log.debug("web-ui: closing main loop")

self._main_loop.quit()

@property
def meh_interface(self):
Expand Down
207 changes: 207 additions & 0 deletions tests/unit_tests/pyanaconda_tests/ui/test_webui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
#
# Copyright (C) 2023 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details. You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
import unittest
import pytest
import tempfile

from meh.ui.text import TextIntf

from pyanaconda.ui.webui import CockpitUserInterface, FIREFOX_THEME_DEFAULT
from unittest.mock import Mock, patch
from pyanaconda.core.constants import PAYLOAD_TYPE_DNF, PAYLOAD_TYPE_LIVE_IMAGE


class SimpleWebUITestCase(unittest.TestCase):
"""Simple test case for Web UI.
The goal of this test is to test execution of the UI behaves as desired.
"""
def setUp(self):
self.intf = CockpitUserInterface(None, None, 0)

def _prepare_for_live_testing(self,
pid_file,
pid_content="",
remote=0):
# prepare UI interface class
self.intf = CockpitUserInterface(None, None, remote)
self.intf._viewer_pid_file = pid_file.name

# wrote pid if requested
if pid_content:
with open(pid_file.name, "wt") as f:
f.write(pid_content)

def test_webui_defaults(self):
"""Test that webui interface has correct defaults."""
assert isinstance(self.intf.meh_interface, TextIntf)

assert self.intf.tty_num == 6

# Not implemented
assert self.intf.showYesNoQuestion("Implemented by browser") is False

@patch("pyanaconda.ui.webui.CockpitUserInterface._print_message")
def test_error_propagation(self, mocked_print_message):
"""Test that webui prints erorr to the console.
This gets then propagated by service to journal.
"""
self.intf.showError("My Error")
mocked_print_message.assert_called_once_with("My Error")

mocked_print_message.reset_mock()
self.intf.showDetailedError("My detailed error", "Such a detail!")
mocked_print_message.assert_called_once_with("My detailed error\n\nSuch a detail!""")

def test_setup(self):
"""Test webui setup call."""
# test not DNF payload type works fine
mocked_payload = Mock()
mocked_payload.type = PAYLOAD_TYPE_LIVE_IMAGE

self.intf = CockpitUserInterface(None, mocked_payload, 0)
self.intf.setup(None)

# test DNF payload raises error because it's not yet implemented
mocked_payload.type = PAYLOAD_TYPE_DNF
self.intf = CockpitUserInterface(None, mocked_payload, 0)
with pytest.raises(ValueError):
self.intf.setup(None)

@patch("pyanaconda.ui.webui.startProgram")
@patch("pyanaconda.ui.webui.conf")
def test_run_not_on_live(self, mocked_conf, mocked_startProgram):
"""Test webui run call on boot iso."""
# Execution is different for boot.iso then live environment
mocked_conf.system.provides_liveuser = False
mocked_process = Mock()
mocked_process.pid = 12345
mocked_startProgram.return_value = mocked_process

with tempfile.NamedTemporaryFile() as ft:
self._prepare_for_live_testing(ft, remote=1)
self.intf.run()

mocked_startProgram.assert_called_once_with(["/usr/libexec/webui-desktop",
"-t", FIREFOX_THEME_DEFAULT, "-r", "1",
"/cockpit/@localhost/anaconda-webui/index.html"],
reset_lang=False)
with open(ft.name, "rt") as f:
assert f.readlines() == ["12345"]

mocked_process.wait.assert_called_once()

# test with disabled remote
mocked_startProgram.reset_mock()
mocked_process.reset_mock()
mocked_startProgram.return_value = mocked_process
with tempfile.NamedTemporaryFile() as ft:
self._prepare_for_live_testing(ft)
self.intf.run()

mocked_startProgram.assert_called_once_with(["/usr/libexec/webui-desktop",
"-t", FIREFOX_THEME_DEFAULT, "-r", "0",
"/cockpit/@localhost/anaconda-webui/index.html"],
reset_lang=False)
with open(ft.name, "rt") as f:
assert f.readlines() == ["12345"]

mocked_process.wait.assert_called_once()

@patch("pyanaconda.ui.webui.PidWatcher.watch_process")
@patch("pyanaconda.ui.webui.create_main_loop")
@patch("pyanaconda.ui.webui.conf")
def test_run_on_live_success(self, mocked_conf, mocked_create_main_loop, mocked_watch_process):
"""Test webui run call on live environment."""
# Execution is different for live because we need to start FF early as possible
mocked_conf.system.provides_liveuser = True
mocked_main_loop = Mock()

# Test on Live media
mocked_watch_process.reset_mock()
mocked_create_main_loop.reset_mock()
mocked_create_main_loop.return_value = mocked_main_loop
mocked_main_loop.reset_mock()
with tempfile.NamedTemporaryFile() as f:
self._prepare_for_live_testing(f, "11111")
self.intf.run()

# check that callback is correctly set
mocked_watch_process.assert_called_once_with(11111, self.intf._webui_desktop_closed)
mocked_create_main_loop.assert_called_once()
mocked_main_loop.run.assert_called_once()

# test quit callbacks by calling them (simple tests avoiding execution of the main loop)
# test webui callback execution - simulates closing the viewer app
self.intf._webui_desktop_closed(11111, 0)
mocked_main_loop.quit.assert_called_once()

# test webui callback bad status - simulates crash of the viewer app
mocked_main_loop.reset_mock()
self.intf._webui_desktop_closed(11111, 1)
mocked_main_loop.quit.assert_called_once()

@patch("pyanaconda.ui.webui.PidWatcher.watch_process")
@patch("pyanaconda.ui.webui.create_main_loop")
@patch("pyanaconda.ui.webui.conf")
def test_run_on_live_failure(self, mocked_conf, mocked_create_main_loop, mocked_watch_process):
"""Test webui run call on live environment."""
mocked_conf.system.provides_liveuser = True
mocked_main_loop = Mock()

# Test empty pid file
mocked_watch_process.reset_mock()
mocked_create_main_loop.reset_mock()
mocked_create_main_loop.return_value = mocked_main_loop
mocked_main_loop.reset_mock()
with tempfile.NamedTemporaryFile() as f:
self._prepare_for_live_testing(f)
with pytest.raises(ValueError):
self.intf.run()

mocked_watch_process.assert_not_called()
mocked_create_main_loop.assert_not_called()

# Test negative pid
mocked_watch_process.reset_mock()
mocked_create_main_loop.reset_mock()
mocked_create_main_loop.return_value = mocked_main_loop
mocked_main_loop.reset_mock()
with tempfile.NamedTemporaryFile() as f:
self._prepare_for_live_testing(f, "-20")

with pytest.raises(ValueError):
self.intf.run()

mocked_watch_process.assert_not_called()
mocked_create_main_loop.assert_not_called()

# Test bad value pid
mocked_watch_process.reset_mock()
mocked_create_main_loop.reset_mock()
mocked_create_main_loop.return_value = mocked_main_loop
mocked_main_loop.reset_mock()
with tempfile.NamedTemporaryFile() as f:
self._prepare_for_live_testing(f, "not-a-number")

with pytest.raises(ValueError):
self.intf.run()

mocked_watch_process.assert_not_called()
mocked_create_main_loop.assert_not_called()

0 comments on commit 5b56da9

Please sign in to comment.