diff --git a/subiquity/common/__init__.py b/subiquity/common/__init__.py
new file mode 100644
index 00000000..8e549e25
--- /dev/null
+++ b/subiquity/common/__init__.py
@@ -0,0 +1,14 @@
+# 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 .
diff --git a/subiquity/common/errorreport.py b/subiquity/common/errorreport.py
new file mode 100644
index 00000000..b5adfc9d
--- /dev/null
+++ b/subiquity/common/errorreport.py
@@ -0,0 +1,412 @@
+# 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 asyncio
+import enum
+import fcntl
+import json
+import logging
+import os
+import sys
+import time
+import traceback
+
+import apport
+import apport.crashdb
+import apport.hookutils
+
+import attr
+
+import bson
+
+import requests
+
+import urwid
+
+from subiquitycore.async_helpers import (
+ run_in_thread,
+ schedule_task,
+ )
+
+log = logging.getLogger('subiquitycore.common.errorreport')
+
+
+class ErrorReportState(enum.Enum):
+ INCOMPLETE = enum.auto()
+ LOADING = enum.auto()
+ DONE = enum.auto()
+ ERROR_GENERATING = enum.auto()
+ ERROR_LOADING = enum.auto()
+
+
+class ErrorReportKind(enum.Enum):
+ BLOCK_PROBE_FAIL = _("Block device probe failure")
+ DISK_PROBE_FAIL = _("Disk probe failure")
+ INSTALL_FAIL = _("Install failure")
+ UI = _("Installer crash")
+ NETWORK_FAIL = _("Network error")
+ UNKNOWN = _("Unknown error")
+
+
+@attr.s(cmp=False)
+class Upload(metaclass=urwid.MetaSignals):
+ signals = ['progress']
+
+ bytes_to_send = attr.ib()
+ bytes_sent = attr.ib(default=0)
+ pipe_r = attr.ib(default=None)
+ pipe_w = attr.ib(default=None)
+ cancelled = attr.ib(default=False)
+
+ def start(self):
+ self.pipe_r, self.pipe_w = os.pipe()
+ fcntl.fcntl(self.pipe_r, fcntl.F_SETFL, os.O_NONBLOCK)
+ asyncio.get_event_loop().add_reader(self.pipe_r, self._progress)
+
+ def _progress(self):
+ os.read(self.pipe_w, 4096)
+ urwid.emit_signal(self, 'progress')
+
+ def _bg_update(self, sent, to_send=None):
+ self.bytes_sent = sent
+ if to_send is not None:
+ self.bytes_to_send = to_send
+ os.write(self.pipe_w, b'x')
+
+ def stop(self):
+ asyncio.get_event_loop().remove_reader(self.pipe_r)
+ os.close(self.pipe_w)
+ os.close(self.pipe_r)
+
+
+@attr.s(cmp=False)
+class ErrorReport(metaclass=urwid.MetaSignals):
+
+ signals = ["changed"]
+
+ reporter = attr.ib()
+ base = attr.ib()
+ pr = attr.ib()
+ state = attr.ib()
+ _file = attr.ib()
+ _context = attr.ib()
+
+ meta = attr.ib(default=attr.Factory(dict))
+ uploader = attr.ib(default=None)
+
+ @classmethod
+ def new(cls, reporter, kind):
+ base = "{:.9f}.{}".format(time.time(), kind.name.lower())
+ crash_file = open(
+ os.path.join(reporter.crash_directory, base + ".crash"),
+ 'wb')
+
+ pr = apport.Report('Bug')
+ pr['CrashDB'] = repr(reporter.crashdb_spec)
+
+ r = cls(
+ reporter=reporter, base=base, pr=pr, file=crash_file,
+ state=ErrorReportState.INCOMPLETE,
+ context=reporter.context.child(base))
+ r.set_meta("kind", kind.name)
+ return r
+
+ @classmethod
+ def from_file(cls, reporter, fpath):
+ base = os.path.splitext(os.path.basename(fpath))[0]
+ report = cls(
+ reporter, base, pr=apport.Report(date='???'),
+ state=ErrorReportState.LOADING, file=open(fpath, 'rb'),
+ context=reporter.context.child(base))
+ try:
+ fp = open(report.meta_path, 'r')
+ except FileNotFoundError:
+ pass
+ else:
+ with fp:
+ report.meta = json.load(fp)
+ return report
+
+ def add_info(self, _bg_attach_hook, wait=False):
+ def _bg_add_info():
+ _bg_attach_hook()
+ # Add basic info to report.
+ self.pr.add_proc_info()
+ self.pr.add_os_info()
+ self.pr.add_hooks_info(None)
+ apport.hookutils.attach_hardware(self.pr)
+ # Because apport-cli will in general be run on a different
+ # machine, we make some slightly obscure alterations to the report
+ # to make this go better.
+
+ # apport-cli gets upset if neither of these are present.
+ self.pr['Package'] = 'subiquity ' + os.environ.get(
+ "SNAP_REVISION", "SNAP_REVISION")
+ self.pr['SourcePackage'] = 'subiquity'
+
+ # If ExecutableTimestamp is present, apport-cli will try to check
+ # that ExecutablePath hasn't changed. But it won't be there.
+ del self.pr['ExecutableTimestamp']
+ # apport-cli gets upset at the probert C extensions it sees in
+ # here. /proc/maps is very unlikely to be interesting for us
+ # anyway.
+ del self.pr['ProcMaps']
+ self.pr.write(self._file)
+
+ async def add_info():
+ with self._context.child("add_info") as context:
+ try:
+ await run_in_thread(_bg_add_info)
+ except Exception:
+ self.state = ErrorReportState.ERROR_GENERATING
+ log.exception("adding info to problem report failed")
+ else:
+ context.description = "written to " + self.path
+ self.state = ErrorReportState.DONE
+ self._file.close()
+ self._file = None
+ urwid.emit_signal(self, "changed")
+ if wait:
+ with self._context.child("add_info") as context:
+ _bg_add_info()
+ context.description = "written to " + self.path
+ else:
+ schedule_task(add_info())
+
+ async def load(self):
+ with self._context.child("load"):
+ # Load report from disk in background.
+ try:
+ await run_in_thread(self.pr.load, self._file)
+ except Exception:
+ log.exception("loading problem report failed")
+ self.state = ErrorReportState.ERROR_LOADING
+ else:
+ self.state = ErrorReportState.DONE
+ self._file.close()
+ self._file = None
+ urwid.emit_signal(self, "changed")
+
+ def upload(self):
+ uploader = self.uploader = Upload(bytes_to_send=1)
+
+ url = "https://daisy.ubuntu.com"
+ if self.reporter.dry_run:
+ url = "https://daisy.staging.ubuntu.com"
+
+ chunk_size = 1024
+
+ def chunk(data):
+ for i in range(0, len(data), chunk_size):
+ if uploader.cancelled:
+ log.debug("upload for %s cancelled", self.base)
+ return
+ yield data[i:i+chunk_size]
+ uploader._bg_update(uploader.bytes_sent + chunk_size)
+
+ def _bg_upload():
+ for_upload = {
+ "Kind": self.kind.value
+ }
+ for k, v in self.pr.items():
+ if len(v) < 1024 or k in {
+ "InstallerLogInfo",
+ "Traceback",
+ "ProcCpuinfoMinimal",
+ }:
+ for_upload[k] = v
+ else:
+ log.debug("dropping %s of length %s", k, len(v))
+ if "CurtinLog" in self.pr:
+ logtail = []
+ for line in self.pr["CurtinLog"].splitlines():
+ logtail.append(line.strip())
+ while sum(map(len, logtail)) > 2048:
+ logtail.pop(0)
+ for_upload["CurtinLogTail"] = "\n".join(logtail)
+ data = bson.BSON().encode(for_upload)
+ self.uploader._bg_update(0, len(data))
+ headers = {
+ 'user-agent': 'subiquity/{}'.format(
+ os.environ.get("SNAP_VERSION", "SNAP_VERSION")),
+ }
+ response = requests.post(url, data=chunk(data), headers=headers)
+ response.raise_for_status()
+ return response.text.split()[0]
+
+ async def upload():
+ with self._context.child("upload") as context:
+ try:
+ oops_id = await run_in_thread(_bg_upload)
+ except requests.exceptions.RequestException:
+ log.exception("upload for %s failed", self.base)
+ else:
+ self.set_meta("oops-id", oops_id)
+ context.description = oops_id
+ uploader.stop()
+ self.uploader = None
+ urwid.emit_signal(self, 'changed')
+
+ urwid.emit_signal(self, 'changed')
+ uploader.start()
+
+ schedule_task(upload())
+
+ def _path_with_ext(self, ext):
+ return os.path.join(
+ self.reporter.crash_directory, self.base + '.' + ext)
+
+ @property
+ def meta_path(self):
+ return self._path_with_ext('meta')
+
+ @property
+ def path(self):
+ return self._path_with_ext('crash')
+
+ def set_meta(self, key, value):
+ self.meta[key] = value
+ with open(self.meta_path, 'w') as fp:
+ json.dump(self.meta, fp, indent=4)
+
+ def mark_seen(self):
+ self.set_meta("seen", True)
+ urwid.emit_signal(self, "changed")
+
+ @property
+ def kind(self):
+ k = self.meta.get("kind", "UNKNOWN")
+ return getattr(ErrorReportKind, k, ErrorReportKind.UNKNOWN)
+
+ @property
+ def seen(self):
+ return self.meta.get("seen", False)
+
+ @property
+ def oops_id(self):
+ return self.meta.get("oops-id")
+
+ @property
+ def persistent_details(self):
+ """Return fs-label, path-on-fs to report."""
+ # Not sure if this is more or less sane than shelling out to
+ # findmnt(1).
+ looking_for = os.path.abspath(
+ os.path.normpath(self.reporter.crash_directory))
+ for line in open('/proc/self/mountinfo').readlines():
+ parts = line.strip().split()
+ if os.path.normpath(parts[4]) == looking_for:
+ devname = parts[9]
+ root = parts[3]
+ break
+ else:
+ if self.reporter.dry_run:
+ path = ('install-logs/2019-11-06.0/crash/' +
+ self.base +
+ '.crash')
+ return "casper-rw", path
+ return None, None
+ import pyudev
+ c = pyudev.Context()
+ devs = list(c.list_devices(
+ subsystem='block', DEVNAME=os.path.realpath(devname)))
+ if not devs:
+ return None, None
+ label = devs[0].get('ID_FS_LABEL_ENC', '')
+ return label, root[1:] + '/' + self.base + '.crash'
+
+
+class ErrorReporter(object):
+
+ def __init__(self, context, dry_run, root):
+ self.context = context
+ self.dry_run = dry_run
+ self.reports = []
+ if dry_run:
+ self.crash_directory = os.path.join(root, 'var/crash')
+ self.crashdb_spec = {
+ 'impl': 'launchpad',
+ 'project': 'subiquity',
+ }
+ if dry_run:
+ self.crashdb_spec['launchpad_instance'] = 'staging'
+ self._apport_data = []
+ self._apport_files = []
+
+ def start_loading_reports(self):
+ os.makedirs(self.crash_directory, exist_ok=True)
+ filenames = os.listdir(self.crash_directory)
+ to_load = []
+ for filename in sorted(filenames, reverse=True):
+ base, ext = os.path.splitext(filename)
+ if ext != ".crash":
+ continue
+ path = os.path.join(self.crash_directory, filename)
+ r = ErrorReport.from_file(self, path)
+ self.reports.append(r)
+ to_load.append(r)
+ schedule_task(self._load_reports(to_load))
+
+ async def _load_reports(self, to_load):
+ for report in to_load:
+ await report.load()
+
+ def note_file_for_apport(self, key, path):
+ self._apport_files.append((key, path))
+
+ def note_data_for_apport(self, key, value):
+ self._apport_data.append((key, value))
+
+ def make_apport_report(self, kind, thing, *, wait=False, **kw):
+ if not self.dry_run and not os.path.exists('/cdrom/.disk/info'):
+ return None
+
+ log.debug("generating crash report")
+
+ try:
+ report = ErrorReport.new(self, kind)
+ self.reports.insert(0, report)
+ except Exception:
+ log.exception("creating crash report failed")
+ return
+
+ etype = sys.exc_info()[0]
+ if etype is not None:
+ report.pr["Title"] = "{} crashed with {}".format(
+ thing, etype.__name__)
+ report.pr['Traceback'] = traceback.format_exc()
+ else:
+ report.pr["Title"] = thing
+
+ log.info(
+ "saving crash report %r to %s", report.pr["Title"], report.path)
+
+ apport_files = self._apport_files[:]
+ apport_data = self._apport_data.copy()
+
+ def _bg_attach_hook():
+ # Attach any stuff other parts of the code think we should know
+ # about.
+ for key, path in apport_files:
+ apport.hookutils.attach_file_if_exists(report.pr, path, key)
+ for key, value in apport_data:
+ report.pr[key] = value
+ for key, value in kw.items():
+ report.pr[key] = value
+
+ report.add_info(_bg_attach_hook, wait)
+
+ # In the fullness of time we should do the signature thing here.
+ return report
diff --git a/subiquity/controllers/error.py b/subiquity/controllers/error.py
index 2810c7e1..cec81a21 100644
--- a/subiquity/controllers/error.py
+++ b/subiquity/controllers/error.py
@@ -13,28 +13,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import enum
-import json
import logging
-import os
-import time
-
-import apport
-import apport.crashdb
-import apport.hookutils
-
-import bson
-
-import attr
-
-import requests
-
-import urwid
-
-from subiquitycore.async_helpers import (
- run_in_thread,
- schedule_task,
- )
from subiquity.controllers.cmdlist import CmdListController
@@ -42,328 +21,7 @@ from subiquity.controllers.cmdlist import CmdListController
log = logging.getLogger('subiquity.controllers.error')
-class ErrorReportState(enum.Enum):
- INCOMPLETE = enum.auto()
- LOADING = enum.auto()
- DONE = enum.auto()
- ERROR_GENERATING = enum.auto()
- ERROR_LOADING = enum.auto()
-
-
-class ErrorReportKind(enum.Enum):
- BLOCK_PROBE_FAIL = _("Block device probe failure")
- DISK_PROBE_FAIL = _("Disk probe failure")
- INSTALL_FAIL = _("Install failure")
- UI = _("Installer crash")
- NETWORK_FAIL = _("Network error")
- UNKNOWN = _("Unknown error")
-
-
-@attr.s(cmp=False)
-class Upload(metaclass=urwid.MetaSignals):
- signals = ['progress']
-
- controller = attr.ib()
- bytes_to_send = attr.ib()
- bytes_sent = attr.ib(default=0)
- pipe_w = attr.ib(default=None)
- cancelled = attr.ib(default=False)
-
- def start(self):
- self.pipe_w = self.controller.app.urwid_loop.watch_pipe(self._progress)
-
- def _progress(self, x):
- urwid.emit_signal(self, 'progress')
-
- def _bg_update(self, sent, to_send=None):
- self.bytes_sent = sent
- if to_send is not None:
- self.bytes_to_send = to_send
- os.write(self.pipe_w, b'x')
-
- def stop(self):
- self.controller.app.urwid_loop.remove_watch_pipe(self.pipe_w)
- os.close(self.pipe_w)
-
-
-@attr.s(cmp=False)
-class ErrorReport(metaclass=urwid.MetaSignals):
-
- signals = ["changed"]
-
- controller = attr.ib()
- base = attr.ib()
- pr = attr.ib()
- state = attr.ib()
- _file = attr.ib()
- _context = attr.ib()
-
- meta = attr.ib(default=attr.Factory(dict))
- uploader = attr.ib(default=None)
-
- @classmethod
- def new(cls, controller, kind):
- base = "{:.9f}.{}".format(time.time(), kind.name.lower())
- crash_file = open(
- os.path.join(controller.crash_directory, base + ".crash"),
- 'wb')
-
- pr = apport.Report('Bug')
- pr['CrashDB'] = repr(controller.crashdb_spec)
-
- r = cls(
- controller=controller, base=base, pr=pr, file=crash_file,
- state=ErrorReportState.INCOMPLETE,
- context=controller.context.child(base))
- r.set_meta("kind", kind.name)
- return r
-
- @classmethod
- def from_file(cls, controller, fpath):
- base = os.path.splitext(os.path.basename(fpath))[0]
- report = cls(
- controller, base, pr=apport.Report(date='???'),
- state=ErrorReportState.LOADING, file=open(fpath, 'rb'),
- context=controller.context.child(base))
- try:
- fp = open(report.meta_path, 'r')
- except FileNotFoundError:
- pass
- else:
- with fp:
- report.meta = json.load(fp)
- return report
-
- def add_info(self, _bg_attach_hook, wait=False):
- def _bg_add_info():
- _bg_attach_hook()
- # Add basic info to report.
- self.pr.add_proc_info()
- self.pr.add_os_info()
- self.pr.add_hooks_info(None)
- apport.hookutils.attach_hardware(self.pr)
- # Because apport-cli will in general be run on a different
- # machine, we make some slightly obscure alterations to the report
- # to make this go better.
-
- # apport-cli gets upset if neither of these are present.
- self.pr['Package'] = 'subiquity ' + os.environ.get(
- "SNAP_REVISION", "SNAP_REVISION")
- self.pr['SourcePackage'] = 'subiquity'
-
- # If ExecutableTimestamp is present, apport-cli will try to check
- # that ExecutablePath hasn't changed. But it won't be there.
- del self.pr['ExecutableTimestamp']
- # apport-cli gets upset at the probert C extensions it sees in
- # here. /proc/maps is very unlikely to be interesting for us
- # anyway.
- del self.pr['ProcMaps']
- self.pr.write(self._file)
-
- async def add_info():
- with self._context.child("add_info") as context:
- try:
- await run_in_thread(_bg_add_info)
- except Exception:
- self.state = ErrorReportState.ERROR_GENERATING
- log.exception("adding info to problem report failed")
- else:
- context.description = "written to " + self.path
- self.state = ErrorReportState.DONE
- self._file.close()
- self._file = None
- urwid.emit_signal(self, "changed")
- if wait:
- with self._context.child("add_info") as context:
- _bg_add_info()
- context.description = "written to " + self.path
- else:
- schedule_task(add_info())
-
- async def load(self):
- with self._context.child("load"):
- # Load report from disk in background.
- try:
- await run_in_thread(self.pr.load, self._file)
- except Exception:
- log.exception("loading problem report failed")
- self.state = ErrorReportState.ERROR_LOADING
- else:
- self.state = ErrorReportState.DONE
- self._file.close()
- self._file = None
- urwid.emit_signal(self, "changed")
-
- def upload(self):
- uploader = self.uploader = Upload(
- controller=self.controller, bytes_to_send=1)
-
- url = "https://daisy.ubuntu.com"
- if self.controller.opts.dry_run:
- url = "https://daisy.staging.ubuntu.com"
-
- chunk_size = 1024
-
- def chunk(data):
- for i in range(0, len(data), chunk_size):
- if uploader.cancelled:
- log.debug("upload for %s cancelled", self.base)
- return
- yield data[i:i+chunk_size]
- uploader._bg_update(uploader.bytes_sent + chunk_size)
-
- def _bg_upload():
- for_upload = {
- "Kind": self.kind.value
- }
- for k, v in self.pr.items():
- if len(v) < 1024 or k in {
- "InstallerLogInfo",
- "Traceback",
- "ProcCpuinfoMinimal",
- }:
- for_upload[k] = v
- else:
- log.debug("dropping %s of length %s", k, len(v))
- if "CurtinLog" in self.pr:
- logtail = []
- for line in self.pr["CurtinLog"].splitlines():
- logtail.append(line.strip())
- while sum(map(len, logtail)) > 2048:
- logtail.pop(0)
- for_upload["CurtinLogTail"] = "\n".join(logtail)
- data = bson.BSON().encode(for_upload)
- self.uploader._bg_update(0, len(data))
- headers = {
- 'user-agent': 'subiquity/{}'.format(
- os.environ.get("SNAP_VERSION", "SNAP_VERSION")),
- }
- response = requests.post(url, data=chunk(data), headers=headers)
- response.raise_for_status()
- return response.text.split()[0]
-
- async def upload():
- with self._context.child("upload") as context:
- try:
- oops_id = await run_in_thread(_bg_upload)
- except requests.exceptions.RequestException:
- log.exception("upload for %s failed", self.base)
- else:
- self.set_meta("oops-id", oops_id)
- context.description = oops_id
- uploader.stop()
- self.uploader = None
- urwid.emit_signal(self, 'changed')
-
- urwid.emit_signal(self, 'changed')
- uploader.start()
-
- schedule_task(upload())
-
- def _path_with_ext(self, ext):
- return os.path.join(
- self.controller.crash_directory, self.base + '.' + ext)
-
- @property
- def meta_path(self):
- return self._path_with_ext('meta')
-
- @property
- def path(self):
- return self._path_with_ext('crash')
-
- def set_meta(self, key, value):
- self.meta[key] = value
- with open(self.meta_path, 'w') as fp:
- json.dump(self.meta, fp, indent=4)
-
- def mark_seen(self):
- self.set_meta("seen", True)
- urwid.emit_signal(self, "changed")
-
- @property
- def kind(self):
- k = self.meta.get("kind", "UNKNOWN")
- return getattr(ErrorReportKind, k, ErrorReportKind.UNKNOWN)
-
- @property
- def seen(self):
- return self.meta.get("seen", False)
-
- @property
- def oops_id(self):
- return self.meta.get("oops-id")
-
- @property
- def persistent_details(self):
- """Return fs-label, path-on-fs to report."""
- # Not sure if this is more or less sane than shelling out to
- # findmnt(1).
- looking_for = os.path.abspath(
- os.path.normpath(self.controller.crash_directory))
- for line in open('/proc/self/mountinfo').readlines():
- parts = line.strip().split()
- if os.path.normpath(parts[4]) == looking_for:
- devname = parts[9]
- root = parts[3]
- break
- else:
- if self.controller.opts.dry_run:
- path = ('install-logs/2019-11-06.0/crash/' +
- self.base +
- '.crash')
- return "casper-rw", path
- return None, None
- import pyudev
- c = pyudev.Context()
- devs = list(c.list_devices(
- subsystem='block', DEVNAME=os.path.realpath(devname)))
- if not devs:
- return None, None
- label = devs[0].get('ID_FS_LABEL_ENC', '')
- return label, root[1:] + '/' + self.base + '.crash'
-
-
class ErrorController(CmdListController):
autoinstall_key = 'error-commands'
cmd_check = False
-
- def __init__(self, app):
- super().__init__(app)
- self.crash_directory = os.path.join(self.app.root, 'var/crash')
- self.crashdb_spec = {
- 'impl': 'launchpad',
- 'project': 'subiquity',
- }
- if self.app.opts.dry_run:
- self.crashdb_spec['launchpad_instance'] = 'staging'
- self.reports = []
-
- def start(self):
- os.makedirs(self.crash_directory, exist_ok=True)
- # scan for pre-existing crash reports and start loading them
- # in the background
- self.scan_crash_dir()
-
- async def _load_reports(self, to_load):
- for report in to_load:
- await report.load()
-
- def scan_crash_dir(self):
- filenames = os.listdir(self.crash_directory)
- to_load = []
- for filename in sorted(filenames, reverse=True):
- base, ext = os.path.splitext(filename)
- if ext != ".crash":
- continue
- path = os.path.join(self.crash_directory, filename)
- r = ErrorReport.from_file(self, path)
- self.reports.append(r)
- to_load.append(r)
- schedule_task(self._load_reports(to_load))
-
- def create_report(self, kind):
- r = ErrorReport.new(self, kind)
- self.reports.insert(0, r)
- return r
diff --git a/subiquity/controllers/filesystem.py b/subiquity/controllers/filesystem.py
index 8f06ae03..c781b60f 100644
--- a/subiquity/controllers/filesystem.py
+++ b/subiquity/controllers/filesystem.py
@@ -33,8 +33,8 @@ from subiquitycore.utils import (
)
+from subiquity.common.errorreport import ErrorReportKind
from subiquity.controller import SubiquityController
-from subiquity.controllers.error import ErrorReportKind
from subiquity.models.filesystem import (
align_up,
Bootloader,
diff --git a/subiquity/controllers/installprogress.py b/subiquity/controllers/installprogress.py
index a26e617d..9addf6e6 100644
--- a/subiquity/controllers/installprogress.py
+++ b/subiquity/controllers/installprogress.py
@@ -43,8 +43,8 @@ from subiquitycore.utils import (
astart_command,
)
+from subiquity.common.errorreport import ErrorReportKind
from subiquity.controller import SubiquityController
-from subiquity.controllers.error import ErrorReportKind
from subiquity.journald import journald_listener
from subiquity.ui.views.installprogress import ProgressView
diff --git a/subiquity/controllers/network.py b/subiquity/controllers/network.py
index 77d599fc..9d8cc266 100644
--- a/subiquity/controllers/network.py
+++ b/subiquity/controllers/network.py
@@ -20,8 +20,8 @@ from subiquitycore.async_helpers import schedule_task
from subiquitycore.context import with_context
from subiquitycore.controllers.network import NetworkController
+from subiquity.common.errorreport import ErrorReportKind
from subiquity.controller import SubiquityController
-from subiquity.controllers.error import ErrorReportKind
log = logging.getLogger("subiquity.controllers.network")
diff --git a/subiquity/core.py b/subiquity/core.py
index 17e51bb3..d254c1f7 100644
--- a/subiquity/core.py
+++ b/subiquity/core.py
@@ -23,8 +23,6 @@ import traceback
import time
import urwid
-import apport.hookutils
-
import jsonschema
import yaml
@@ -42,7 +40,8 @@ from subiquitycore.snapd import (
)
from subiquitycore.view import BaseView
-from subiquity.controllers.error import (
+from subiquity.common.errorreport import (
+ ErrorReporter,
ErrorReportKind,
)
from subiquity.journald import journald_listener
@@ -147,14 +146,15 @@ class Subiquity(Application):
('network-proxy-set', lambda: schedule_task(self._proxy_set())),
('network-change', self._network_change),
])
- self._apport_data = []
- self._apport_files = []
self.autoinstall_config = {}
self.report_to_show = None
self.show_progress_handle = None
self.progress_shown_time = self.aio_loop.time()
self.progress_showing = False
+ self.error_reporter = ErrorReporter(
+ self.context.child("ErrorReporter"), self.opts.dry_run, self.root)
+
self.note_data_for_apport("SnapUpdated", str(self.updated))
self.note_data_for_apport("UsingAnswers", str(bool(self.answers)))
@@ -363,7 +363,8 @@ class Subiquity(Application):
self.ui.body.remove_overlay(overlay)
def select_initial_screen(self, index):
- for report in self.controllers.Error.reports:
+ self.error_reporter.start_loading_reports()
+ for report in self.error_reporter.reports:
if report.kind == ErrorReportKind.UI and not report.seen:
self.show_error_report(report)
break
@@ -456,53 +457,18 @@ class Subiquity(Application):
["bash"], before_hook=_before, after_hook=after_hook, cwd='/')
def note_file_for_apport(self, key, path):
- self._apport_files.append((key, path))
+ self.error_reporter.note_file_for_apport(key, path)
def note_data_for_apport(self, key, value):
- self._apport_data.append((key, value))
+ self.error_reporter.note_data_for_apport(key, value)
def make_apport_report(self, kind, thing, *, interrupt, wait=False, **kw):
- if not self.opts.dry_run and not os.path.exists('/cdrom/.disk/info'):
- return None
+ report = self.error_reporter.make_apport_report(
+ kind, thing, wait=wait, **kw)
- log.debug("generating crash report")
-
- try:
- report = self.controllers.Error.create_report(kind)
- except Exception:
- log.exception("creating crash report failed")
- return
-
- etype = sys.exc_info()[0]
- if etype is not None:
- report.pr["Title"] = "{} crashed with {}".format(
- thing, etype.__name__)
- report.pr['Traceback'] = traceback.format_exc()
- else:
- report.pr["Title"] = thing
-
- log.info(
- "saving crash report %r to %s", report.pr["Title"], report.path)
-
- apport_files = self._apport_files[:]
- apport_data = self._apport_data.copy()
-
- def _bg_attach_hook():
- # Attach any stuff other parts of the code think we should know
- # about.
- for key, path in apport_files:
- apport.hookutils.attach_file_if_exists(report.pr, path, key)
- for key, value in apport_data:
- report.pr[key] = value
- for key, value in kw.items():
- report.pr[key] = value
-
- report.add_info(_bg_attach_hook, wait)
-
- if interrupt and self.interactive():
+ if report is not None and interrupt and self.interactive():
self.show_error_report(report)
- # In the fullness of time we should do the signature thing here.
return report
def show_error_report(self, report):
diff --git a/subiquity/ui/views/error.py b/subiquity/ui/views/error.py
index 677462a4..89f5d33b 100644
--- a/subiquity/ui/views/error.py
+++ b/subiquity/ui/views/error.py
@@ -45,7 +45,7 @@ from subiquitycore.ui.width import (
widget_width,
)
-from subiquity.controllers.error import (
+from subiquity.common.errorreport import (
ErrorReportKind,
ErrorReportState,
)
@@ -285,7 +285,7 @@ class ErrorReportListStretchy(Stretchy):
Text(""),
])]
self.report_to_row = {}
- for report in self.app.controllers.Error.reports:
+ for report in self.app.error_reporter.reports:
connect_signal(report, "changed", self._report_changed, report)
r = self.report_to_row[report] = self.row_for_report(report)
rows.append(r)
diff --git a/subiquity/ui/views/help.py b/subiquity/ui/views/help.py
index 88ed7b07..9692e0b9 100644
--- a/subiquity/ui/views/help.py
+++ b/subiquity/ui/views/help.py
@@ -302,7 +302,7 @@ class OpenHelpMenu(WidgetWrap):
local = Text(
('info_minor header', " " + _("Help on this screen") + " "))
- if self.parent.app.controllers.Error.reports:
+ if self.parent.app.error_reporter.reports:
view_errors = menu_item(
_("View error reports").format(local_title),
on_press=self.parent.show_errors)