Merge pull request #806 from mwhudson/add-subiquity.common

add subiqity.common package and more error report handling to it
This commit is contained in:
Michael Hudson-Doyle 2020-08-23 22:44:43 +12:00 committed by GitHub
commit 2e20c5d094
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 444 additions and 394 deletions

View File

@ -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 <http://www.gnu.org/licenses/>.

View File

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -13,28 +13,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import enum
import json
import logging 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 from subiquity.controllers.cmdlist import CmdListController
@ -42,328 +21,7 @@ from subiquity.controllers.cmdlist import CmdListController
log = logging.getLogger('subiquity.controllers.error') 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): class ErrorController(CmdListController):
autoinstall_key = 'error-commands' autoinstall_key = 'error-commands'
cmd_check = False 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

View File

@ -33,8 +33,8 @@ from subiquitycore.utils import (
) )
from subiquity.common.errorreport import ErrorReportKind
from subiquity.controller import SubiquityController from subiquity.controller import SubiquityController
from subiquity.controllers.error import ErrorReportKind
from subiquity.models.filesystem import ( from subiquity.models.filesystem import (
align_up, align_up,
Bootloader, Bootloader,

View File

@ -43,8 +43,8 @@ from subiquitycore.utils import (
astart_command, astart_command,
) )
from subiquity.common.errorreport import ErrorReportKind
from subiquity.controller import SubiquityController from subiquity.controller import SubiquityController
from subiquity.controllers.error import ErrorReportKind
from subiquity.journald import journald_listener from subiquity.journald import journald_listener
from subiquity.ui.views.installprogress import ProgressView from subiquity.ui.views.installprogress import ProgressView

View File

@ -20,8 +20,8 @@ from subiquitycore.async_helpers import schedule_task
from subiquitycore.context import with_context from subiquitycore.context import with_context
from subiquitycore.controllers.network import NetworkController from subiquitycore.controllers.network import NetworkController
from subiquity.common.errorreport import ErrorReportKind
from subiquity.controller import SubiquityController from subiquity.controller import SubiquityController
from subiquity.controllers.error import ErrorReportKind
log = logging.getLogger("subiquity.controllers.network") log = logging.getLogger("subiquity.controllers.network")

View File

@ -23,8 +23,6 @@ import traceback
import time import time
import urwid import urwid
import apport.hookutils
import jsonschema import jsonschema
import yaml import yaml
@ -42,7 +40,8 @@ from subiquitycore.snapd import (
) )
from subiquitycore.view import BaseView from subiquitycore.view import BaseView
from subiquity.controllers.error import ( from subiquity.common.errorreport import (
ErrorReporter,
ErrorReportKind, ErrorReportKind,
) )
from subiquity.journald import journald_listener from subiquity.journald import journald_listener
@ -147,14 +146,15 @@ class Subiquity(Application):
('network-proxy-set', lambda: schedule_task(self._proxy_set())), ('network-proxy-set', lambda: schedule_task(self._proxy_set())),
('network-change', self._network_change), ('network-change', self._network_change),
]) ])
self._apport_data = []
self._apport_files = []
self.autoinstall_config = {} self.autoinstall_config = {}
self.report_to_show = None self.report_to_show = None
self.show_progress_handle = None self.show_progress_handle = None
self.progress_shown_time = self.aio_loop.time() self.progress_shown_time = self.aio_loop.time()
self.progress_showing = False 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("SnapUpdated", str(self.updated))
self.note_data_for_apport("UsingAnswers", str(bool(self.answers))) self.note_data_for_apport("UsingAnswers", str(bool(self.answers)))
@ -363,7 +363,8 @@ class Subiquity(Application):
self.ui.body.remove_overlay(overlay) self.ui.body.remove_overlay(overlay)
def select_initial_screen(self, index): 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: if report.kind == ErrorReportKind.UI and not report.seen:
self.show_error_report(report) self.show_error_report(report)
break break
@ -456,53 +457,18 @@ class Subiquity(Application):
["bash"], before_hook=_before, after_hook=after_hook, cwd='/') ["bash"], before_hook=_before, after_hook=after_hook, cwd='/')
def note_file_for_apport(self, key, path): 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): 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): 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'): report = self.error_reporter.make_apport_report(
return None kind, thing, wait=wait, **kw)
log.debug("generating crash report") if report is not None and interrupt and self.interactive():
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():
self.show_error_report(report) self.show_error_report(report)
# In the fullness of time we should do the signature thing here.
return report return report
def show_error_report(self, report): def show_error_report(self, report):

View File

@ -45,7 +45,7 @@ from subiquitycore.ui.width import (
widget_width, widget_width,
) )
from subiquity.controllers.error import ( from subiquity.common.errorreport import (
ErrorReportKind, ErrorReportKind,
ErrorReportState, ErrorReportState,
) )
@ -285,7 +285,7 @@ class ErrorReportListStretchy(Stretchy):
Text(""), Text(""),
])] ])]
self.report_to_row = {} 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) connect_signal(report, "changed", self._report_changed, report)
r = self.report_to_row[report] = self.row_for_report(report) r = self.report_to_row[report] = self.row_for_report(report)
rows.append(r) rows.append(r)

View File

@ -302,7 +302,7 @@ class OpenHelpMenu(WidgetWrap):
local = Text( local = Text(
('info_minor header', " " + _("Help on this screen") + " ")) ('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_errors = menu_item(
_("View error reports").format(local_title), _("View error reports").format(local_title),
on_press=self.parent.show_errors) on_press=self.parent.show_errors)