Merge pull request #738 from mwhudson/stop-all-probing-on-install

Stop all probing during installation
This commit is contained in:
Michael Hudson-Doyle 2020-05-01 11:45:04 +12:00 committed by GitHub
commit f9eb715108
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 242 additions and 67 deletions

View File

@ -106,6 +106,7 @@ class FilesystemController(SubiquityController):
self.stop_listening_udev()
await self._start_task
await self._probe_task.wait()
self.convert_autoinstall_config()
if not self.model.is_root_mounted():
raise Exception("autoinstall config did not mount root")
if self.model.needs_bootloader_partition():
@ -132,36 +133,36 @@ class FilesystemController(SubiquityController):
async def _probe(self):
with self.context.child("_probe") as context:
self._crash_reports = {}
if isinstance(self.ui.body, ProbingFailed):
self.ui.set_body(SlowProbing(self))
schedule_task(self._wait_for_probing())
for (restricted, kind) in [
(False, ErrorReportKind.BLOCK_PROBE_FAIL),
(True, ErrorReportKind.DISK_PROBE_FAIL),
]:
try:
desc = "restricted={}".format(restricted)
with context.child("probe_once", desc):
await self._probe_once_task.start(restricted)
# We wait on the task directly here, not
# self._probe_once_task.wait as if _probe_once_task
# gets cancelled, we should be cancelled too.
await asyncio.wait_for(
self._probe_once_task.task, 15.0)
except asyncio.CancelledError:
# asyncio.CancelledError is a subclass of Exception in
# Python 3.6 (sadface)
raise
except Exception:
block_discover_log.exception(
"block probing failed restricted=%s", restricted)
report = self.app.make_apport_report(
kind, "block probing", interrupt=False)
self._crash_reports[restricted] = report
continue
break
self.convert_autoinstall_config()
async with self.app.install_lock_file.shared():
self._crash_reports = {}
if isinstance(self.ui.body, ProbingFailed):
self.ui.set_body(SlowProbing(self))
schedule_task(self._wait_for_probing())
for (restricted, kind) in [
(False, ErrorReportKind.BLOCK_PROBE_FAIL),
(True, ErrorReportKind.DISK_PROBE_FAIL),
]:
try:
desc = "restricted={}".format(restricted)
with context.child("probe_once", desc):
await self._probe_once_task.start(restricted)
# We wait on the task directly here, not
# self._probe_once_task.wait as if _probe_once_task
# gets cancelled, we should be cancelled too.
await asyncio.wait_for(
self._probe_once_task.task, 15.0)
except asyncio.CancelledError:
# asyncio.CancelledError is a subclass of Exception in
# Python 3.6 (sadface)
raise
except Exception:
block_discover_log.exception(
"block probing failed restricted=%s", restricted)
report = self.app.make_apport_report(
kind, "block probing", interrupt=False)
self._crash_reports[restricted] = report
continue
break
def convert_autoinstall_config(self):
log.debug("self.ai_data = %s", self.ai_data)
@ -187,8 +188,7 @@ class FilesystemController(SubiquityController):
self._monitor = pyudev.Monitor.from_netlink(context)
self._monitor.filter_by(subsystem='block')
self._monitor.enable_receiving()
if self.app.interactive():
self.start_listening_udev()
self.start_listening_udev()
await self._probe_task.start()
def start_listening_udev(self):
@ -235,6 +235,7 @@ class FilesystemController(SubiquityController):
# events as merging system changes with configuration the user has
# performed would be tricky. Possibly worth doing though! Just
# not today.
self.convert_autoinstall_config()
self.stop_listening_udev()
self.ui.set_body(GuidedDiskSelectionView(self))
pr = self._crash_reports.get(False)

View File

@ -33,6 +33,7 @@ from curtin.util import write_file
from systemd import journal
import yaml
from subiquitycore.async_helpers import (
run_in_thread,
schedule_task,
@ -45,6 +46,7 @@ from subiquitycore.utils import (
from subiquity.controller import SubiquityController
from subiquity.controllers.error import ErrorReportKind
from subiquity.journald import journald_listener
from subiquity.ui.views.installprogress import ProgressView
@ -97,7 +99,6 @@ class InstallProgressController(SubiquityController):
self.model = app.base_model
self.progress_view = ProgressView(self)
self.install_state = InstallState.NOT_STARTED
self.journal_listener_handle = None
self.reboot_clicked = asyncio.Event()
if self.answers.get('reboot', False):
@ -202,21 +203,6 @@ class InstallProgressController(SubiquityController):
self.progress_view.add_log_line(log_line)
self.tb_extractor.feed(log_line)
def start_journald_listener(self, identifiers, callback):
reader = journal.Reader()
args = []
for identifier in identifiers:
args.append("SYSLOG_IDENTIFIER={}".format(identifier))
reader.add_match(*args)
def watch():
if reader.process() != journal.APPEND:
return
for event in reader:
callback(event)
loop = asyncio.get_event_loop()
return loop.add_reader(reader.fileno(), watch)
def _write_config(self, path, config):
with open(path, 'w') as conf:
datestr = '# Autogenerated by SUbiquity: {} UTC\n'.format(
@ -272,16 +258,25 @@ class InstallProgressController(SubiquityController):
self.install_state = InstallState.RUNNING
self.curtin_event_contexts[''] = context
self.journal_listener_handle = self.start_journald_listener(
journal_fd, watcher = journald_listener(
[self._event_syslog_identifier, self._log_syslog_identifier],
self._journal_event)
self.app.aio_loop.add_reader(journal_fd, watcher)
curtin_cmd = self._get_curtin_command()
log.debug('curtin install cmd: {}'.format(curtin_cmd))
cp = await arun_command(
self.logged_command(curtin_cmd), check=True)
async with self.app.install_lock_file.exclusive():
try:
our_tty = os.ttyname(0)
except OSError:
# This is a gross hack for testing in travis.
our_tty = "/dev/not a tty"
self.app.install_lock_file.write_content(our_tty)
journal.send("starting install", SYSLOG_IDENTIFIER="subiquity")
cp = await arun_command(
self.logged_command(curtin_cmd), check=True)
log.debug('curtin_install completed: %s', cp.returncode)

View File

@ -205,8 +205,7 @@ class RefreshController(SubiquityController):
return CheckState.UNAVAILABLE
async def start_update(self):
update_marker = os.path.join(self.app.state_dir, 'updating')
open(update_marker, 'w').close()
open(self.app.state_path('updating'), 'w').close()
with self.context.child("starting_update") as context:
change = await self.app.snapd.post(
'v2/snaps/{}'.format(self.snap_name),

View File

@ -39,6 +39,8 @@ from subiquitycore.core import Application
from subiquity.controllers.error import (
ErrorReportKind,
)
from subiquity.journald import journald_listener
from subiquity.lockfile import Lockfile
from subiquity.models.subiquity import SubiquityModel
from subiquity.snapd import (
AsyncSnapd,
@ -122,7 +124,11 @@ class Subiquity(Application):
if not opts.bootloader == 'none' and platform.machine() != 's390x':
self.controllers.remove("Zdev")
self.journal_fd, self.journal_watcher = journald_listener(
["subiquity"], self.subiquity_event, seek=True)
super().__init__(opts)
self.install_lock_file = Lockfile(self.state_path("installing"))
self.install_running = None
self.block_log_dir = block_log_dir
self.kernel_cmdline = shlex.split(opts.kernel_cmdline)
if opts.snaps_from_examples:
@ -152,6 +158,27 @@ class Subiquity(Application):
self.install_confirmed = False
def subiquity_event(self, event):
if event["MESSAGE"] == "starting install":
if event["_PID"] == os.getpid():
return
if not self.install_lock_file.is_exclusively_locked():
return
from subiquity.ui.views.installprogress import (
InstallRunning,
)
tty = self.install_lock_file.read_content()
self.install_running = InstallRunning(self.ui.body, self, tty)
self.ui.body.show_stretchy_overlay(self.install_running)
schedule_task(self._hide_install_running())
async def _hide_install_running(self):
# Wait until the install has completed...
async with self.install_lock_file.shared():
# And remove the overlay.
self.install_running = None
self.ui.body.remove_overlay()
def restart(self, remove_last_screen=True):
if remove_last_screen:
self._remove_last_screen()
@ -201,7 +228,7 @@ class Subiquity(Application):
jsonschema.validate(self.autoinstall_config, self.base_schema)
self.controllers.load("Early")
if self.controllers.Early.cmds:
stamp_file = os.path.join(self.state_dir, "early-commands")
stamp_file = self.state_path("early-commands")
if our_tty != primary_tty:
print(
_("waiting for installer running on {} to run early "
@ -229,6 +256,10 @@ class Subiquity(Application):
# in next_screen below will be confusing.
os.system('stty sane')
def new_event_loop(self):
super().new_event_loop()
self.aio_loop.add_reader(self.journal_fd, self.journal_watcher)
def run(self):
try:
if self.opts.autoinstall is not None:
@ -363,6 +394,8 @@ class Subiquity(Application):
log.debug("showing new error %r", self.report_to_show.base)
self.show_error_report(self.report_to_show)
self.report_to_show = None
if self.install_running is not None:
self.ui.body.show_stretchy_overlay(self.install_running)
elif self.autoinstall_config and not new.autoinstall_applied:
if self.interactive() and self.show_progress_handle is None:
self.ui.block_input = True

34
subiquity/journald.py Normal file
View File

@ -0,0 +1,34 @@
# Copyright 2020 Canonical, Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from systemd import journal
def journald_listener(identifiers, callback, seek=False):
reader = journal.Reader()
args = []
for identifier in identifiers:
args.append("SYSLOG_IDENTIFIER={}".format(identifier))
reader.add_match(*args)
if seek:
reader.seek_tail()
def watch():
if reader.process() != journal.APPEND:
return
for event in reader:
callback(event)
return reader.fileno(), watch

80
subiquity/lockfile.py Normal file
View File

@ -0,0 +1,80 @@
# Copyright 2020 Canonical, Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import fcntl
import logging
from subiquitycore.async_helpers import run_in_thread
log = logging.getLogger('subiquity.lockfile')
class _LockContext:
def __init__(self, lockfile, flags):
self.lockfile = lockfile
self.flags = flags
self._kind = "???"
if flags & fcntl.LOCK_EX:
self._kind = "exclusive"
elif flags & fcntl.LOCK_SH:
self._kind = "shared"
def __enter__(self):
log.debug("locking %s %s", self._kind, self.lockfile.path)
fcntl.flock(self.lockfile.fp, self.flags)
return self
async def __aenter__(self):
return await run_in_thread(self.__enter__)
def __exit__(self, etype, evalue, etb):
log.debug("unlocking %s %s", self._kind, self.lockfile.path)
fcntl.flock(self.lockfile.fp, fcntl.LOCK_UN)
async def __aexit__(self, etype, evalue, etb):
self.__exit__(etype, evalue, etb)
class Lockfile:
def __init__(self, path):
self.path = path
self.fp = open(path, 'a+')
def read_content(self):
self.fp.seek(0)
return self.fp.read()
def write_content(self, content):
self.fp.seek(0)
self.fp.truncate()
self.fp.write(content)
self.fp.flush()
def exclusive(self):
return _LockContext(self, fcntl.LOCK_EX)
def shared(self):
return _LockContext(self, fcntl.LOCK_SH)
def is_exclusively_locked(self):
try:
fcntl.flock(self.fp, fcntl.LOCK_SH | fcntl.LOCK_NB)
except OSError:
return True
else:
fcntl.flock(self.fp, fcntl.LOCK_UN)
return False

View File

@ -33,6 +33,7 @@ from subiquitycore.ui.utils import button_pile, Padding, rewrap
from subiquitycore.ui.stretchy import Stretchy
from subiquitycore.ui.width import widget_width
log = logging.getLogger("subiquity.views.installprogress")
@ -227,3 +228,36 @@ class InstallConfirmation(Stretchy):
self.parent.remove_overlay()
if isinstance(self.parent, ProgressView):
self.parent.show_continue()
running_text = _("""\
The installer running on {tty} is currently installing the system.
You can wait for this to complete or switch to a shell.
""")
class InstallRunning(Stretchy):
def __init__(self, parent, app, tty):
self.parent = parent
self.app = app
self.btn = Toggleable(other_btn(
_("Switch to a shell"), on_press=self._debug_shell))
self.btn.enabled = False
self.app.aio_loop.call_later(0.5, self._enable)
widgets = [
Text(rewrap(_(running_text).format(tty=tty))),
Text(''),
button_pile([self.btn]),
]
super().__init__(
_(""),
widgets,
stretchy_index=0,
focus_index=2)
def _enable(self):
self.btn.enabled = True
def _debug_shell(self, sender):
self.app.debug_shell()

View File

@ -339,7 +339,7 @@ class Application:
if opts.dry_run:
self.root = '.subiquity'
self.state_dir = os.path.join(self.root, 'run', self.project)
os.makedirs(os.path.join(self.state_dir, 'states'), exist_ok=True)
os.makedirs(self.state_path('states'), exist_ok=True)
self.answers = {}
if opts.answers is not None:
@ -360,7 +360,7 @@ class Application:
self.scale_factor = float(
os.environ.get('SUBIQUITY_REPLAY_TIMESCALE', "1"))
self.updated = os.path.exists(os.path.join(self.state_dir, 'updating'))
self.updated = os.path.exists(self.state_path('updating'))
self.signal = Signal()
self.prober = prober
self.new_event_loop()
@ -417,13 +417,14 @@ class Application:
controller.register_signals()
log.debug("known signals: %s", self.signal.known_signals)
def state_path(self, *parts):
return os.path.join(self.state_dir, *parts)
def save_state(self):
cur = self.controllers.cur
if cur is None:
return
state_path = os.path.join(
self.state_dir, 'states', cur.name)
with open(state_path, 'w') as fp:
with open(self.state_path('states', cur.name), 'w') as fp:
json.dump(cur.serialize(), fp)
def select_screen(self, new):
@ -435,8 +436,7 @@ class Application:
except Skip:
new.context.exit("(skipped)")
raise
state_path = os.path.join(self.state_dir, 'last-screen')
with open(state_path, 'w') as fp:
with open(self.state_path('last-screen'), 'w') as fp:
fp.write(new.name)
def _move_screen(self, increment):
@ -485,9 +485,9 @@ class Application:
# EventLoop -------------------------------------------------------------------
def _remove_last_screen(self):
state_path = os.path.join(self.state_dir, 'last-screen')
if os.path.exists(state_path):
os.unlink(state_path)
last_screen = self.state_path('last-screen')
if os.path.exists(last_screen):
os.unlink(last_screen)
def exit(self):
self._remove_last_screen()
@ -583,15 +583,14 @@ class Application:
def load_serialized_state(self):
for controller in self.controllers.instances:
state_path = os.path.join(
self.state_dir, 'states', controller.name)
state_path = self.state_path('states', controller.name)
if not os.path.exists(state_path):
continue
with open(state_path) as fp:
controller.deserialize(json.load(fp))
last_screen = None
state_path = os.path.join(self.state_dir, 'last-screen')
state_path = self.state_path('last-screen')
if os.path.exists(state_path):
with open(state_path) as fp:
last_screen = fp.read().strip()