Merge pull request #738 from mwhudson/stop-all-probing-on-install
Stop all probing during installation
This commit is contained in:
commit
f9eb715108
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue