diff --git a/subiquity/controllers/filesystem.py b/subiquity/controllers/filesystem.py
index f8350d3a..1ba71f67 100644
--- a/subiquity/controllers/filesystem.py
+++ b/subiquity/controllers/filesystem.py
@@ -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)
diff --git a/subiquity/controllers/installprogress.py b/subiquity/controllers/installprogress.py
index 8e2ccf0f..5078baf4 100644
--- a/subiquity/controllers/installprogress.py
+++ b/subiquity/controllers/installprogress.py
@@ -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)
diff --git a/subiquity/controllers/refresh.py b/subiquity/controllers/refresh.py
index e85b3373..2860409f 100644
--- a/subiquity/controllers/refresh.py
+++ b/subiquity/controllers/refresh.py
@@ -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),
diff --git a/subiquity/core.py b/subiquity/core.py
index cb329e25..eef9fd7c 100644
--- a/subiquity/core.py
+++ b/subiquity/core.py
@@ -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
diff --git a/subiquity/journald.py b/subiquity/journald.py
new file mode 100644
index 00000000..7c4b3f2f
--- /dev/null
+++ b/subiquity/journald.py
@@ -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 .
+
+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
diff --git a/subiquity/lockfile.py b/subiquity/lockfile.py
new file mode 100644
index 00000000..f6b75aeb
--- /dev/null
+++ b/subiquity/lockfile.py
@@ -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 .
+
+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
diff --git a/subiquity/ui/views/installprogress.py b/subiquity/ui/views/installprogress.py
index c22c2f70..8b4d0098 100644
--- a/subiquity/ui/views/installprogress.py
+++ b/subiquity/ui/views/installprogress.py
@@ -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()
diff --git a/subiquitycore/core.py b/subiquitycore/core.py
index 43bc94f2..ebb9ef48 100644
--- a/subiquitycore/core.py
+++ b/subiquitycore/core.py
@@ -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()