Skip to content

Commit 7cdda8e

Browse files
nk3: Improve error handling during firmware update
This patch improves the error handling for firmware updates. First of all, it actually shows the error message to the user instead of just logging it. Also, it adds a distinction between failed updates and aborted updates (due to a user decision). Fixes: #296 Fixes: #339
1 parent 8fe7a6f commit 7cdda8e

File tree

4 files changed

+87
-53
lines changed

4 files changed

+87
-53
lines changed

nitrokeyapp/device_data.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from nitrokey.trussed import TrussedBase, TrussedDevice, Uuid, Version
77
from nitrokey.trussed.admin_app import Status
88

9-
from nitrokeyapp.update import Nk3Context, UpdateGUI
9+
from nitrokeyapp.update import Nk3Context, UpdateGUI, UpdateResult, UpdateStatus
1010

1111
logger = logging.getLogger(__name__)
1212

@@ -106,14 +106,12 @@ def update(
106106
self,
107107
ui: UpdateGUI,
108108
image: Optional[str] = None,
109-
) -> bool:
109+
) -> UpdateResult:
110110
self.updating = True
111-
try:
112-
Nk3Context(self.path).update(ui, image)
111+
result = Nk3Context(self.path).update(ui, image)
112+
if result.status == UpdateStatus.SUCCESS:
113113
logger.info("Nitrokey 3 successfully updated")
114-
self.updating = False
115-
return True
116-
except Exception as e:
117-
logger.info(f"Nitrokey 3 failed to update - {e}")
118-
self.updating = False
119-
return False
114+
else:
115+
logger.error(f"Nitrokey 3 update failed: {result}")
116+
self.updating = False
117+
return result

nitrokeyapp/overview_tab/__init__.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from nitrokeyapp.common_ui import CommonUi
1010
from nitrokeyapp.device_data import DeviceData
1111
from nitrokeyapp.qt_utils_mix_in import QtUtilsMixIn
12+
from nitrokeyapp.update import UpdateResult, UpdateStatus
1213
from nitrokeyapp.worker import Worker
1314

1415
from .worker import OverviewWorker
@@ -208,13 +209,24 @@ def run_update(self) -> None:
208209

209210
self.trigger_update.emit(self.data, self.is_qubesos)
210211

211-
@Slot(bool)
212-
def device_updated(self, success: bool) -> None:
212+
@Slot(UpdateResult)
213+
def device_updated(self, result: UpdateResult) -> None:
213214
self.update_btns_during_update(True)
214-
if success:
215-
self.common_ui.info.info.emit("Nitrokey 3 successfully updated")
215+
216+
msg = ""
217+
if result.message is not None:
218+
msg = ": " + result.message
219+
220+
if result.status == UpdateStatus.SUCCESS:
221+
self.common_ui.info.info.emit(f"Nitrokey 3 successfully updated{msg}")
222+
elif result.status == UpdateStatus.ERROR:
223+
self.common_ui.info.error.emit(f"Nitrokey 3 update failed{msg}")
224+
elif result.status == UpdateStatus.ABORTED:
225+
self.common_ui.info.error.emit(f"Nitrokey 3 update aborted{msg}")
216226
else:
217-
self.common_ui.info.error.emit("Nitrokey 3 update failed")
227+
self.common_ui.info.error.emit(
228+
f"Unexpected update result: {result.status}{msg}"
229+
)
218230

219231
self.common_ui.gui.refresh_devices.emit()
220232

nitrokeyapp/overview_tab/worker.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55

66
from nitrokeyapp.common_ui import CommonUi
77
from nitrokeyapp.device_data import DeviceData
8-
from nitrokeyapp.update import UpdateGUI
8+
from nitrokeyapp.update import UpdateGUI, UpdateResult
99
from nitrokeyapp.worker import Job, Worker
1010

1111
logger = logging.getLogger(__name__)
1212

1313

1414
class UpdateDevice(Job):
15-
device_updated = Signal(bool)
15+
device_updated = Signal(UpdateResult)
1616

1717
def __init__(
1818
self,
@@ -34,11 +34,11 @@ def __init__(
3434

3535
def run(self) -> None:
3636
if not self.image:
37-
success = self.data.update(self.update_gui)
37+
result = self.data.update(self.update_gui)
3838
else:
39-
success = self.data.update(self.update_gui, self.image)
39+
result = self.data.update(self.update_gui, self.image)
4040

41-
self.device_updated.emit(success)
41+
self.device_updated.emit(result)
4242

4343
@Slot()
4444
def cleanup(self) -> None:
@@ -51,7 +51,7 @@ def cancel_busy_wait(self, confirmed: bool) -> None:
5151

5252
class OverviewWorker(Worker):
5353
# TODO: remove DeviceData from signatures
54-
device_updated = Signal(bool)
54+
device_updated = Signal(UpdateResult)
5555

5656
def __init__(self, common_ui: CommonUi) -> None:
5757
super().__init__(common_ui)

nitrokeyapp/update.py

Lines changed: 56 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import logging
22
from contextlib import contextmanager
3+
from dataclasses import dataclass
4+
from enum import Enum
35
from time import sleep
46
from typing import (
57
TYPE_CHECKING,
@@ -29,25 +31,45 @@
2931
T = TypeVar("T", bound=TrussedBase)
3032

3133

34+
class UpdateStatus(Enum):
35+
SUCCESS = "success"
36+
ERROR = "error"
37+
ABORTED = "aborted"
38+
39+
40+
@dataclass
41+
class UpdateResult:
42+
status: UpdateStatus
43+
message: str | None = None
44+
45+
46+
@dataclass
47+
class UpdateException(Exception):
48+
def __init__(self, status: UpdateStatus, *msgs: Any) -> None:
49+
super().__init__(*msgs)
50+
self.status = status
51+
52+
3253
class UpdateGUI(UpdateUi):
3354
def __init__(self, common_ui: "CommonUi", is_qubesos: bool) -> None:
3455
super().__init__()
3556

36-
self._version_printed = False
3757
self.common_ui = common_ui
3858
self.is_qubesos = is_qubesos
3959

4060
# blocking wait, set by parent during confirm-prompt
4161
self.await_confirmation: Optional[bool] = None
4262

4363
def error(self, *msgs: Any) -> Exception:
44-
return Exception(*msgs)
64+
logger.error(f"Error during firmware update: {msgs}")
65+
return UpdateException(UpdateStatus.ERROR, *msgs)
4566

4667
def abort(self, *msgs: Any) -> Exception:
47-
return Exception(*msgs)
68+
logger.warning(f"Firmware update aborted: {msgs}")
69+
return UpdateException(UpdateStatus.ABORTED, *msgs)
4870

4971
def raise_warning(self, warning: Warning) -> Exception:
50-
return Exception(warning.message)
72+
return UpdateException(UpdateStatus.ERROR, warning.message)
5173

5274
def show_warning(self, warning: Warning) -> None:
5375
res = self.run_confirm_dialog(
@@ -60,8 +82,9 @@ def show_warning(self, warning: Warning) -> None:
6082
logger.info("OK clicked (warning dialog)")
6183

6284
def abort_downgrade(self, current: Version, image: Version) -> Exception:
63-
self._print_firmware_versions(current, image)
64-
return self.abort("Abort: firmware older as on device")
85+
return self.abort(
86+
f"firmware {image} is older than the firmware on the device ({current})"
87+
)
6588

6689
def run_confirm_dialog(self, title: str, desc: str) -> bool:
6790
self.common_ui.prompt.confirm.emit(title, desc)
@@ -106,7 +129,7 @@ def confirm_update(self, current: Optional[Version], new: Version) -> None:
106129
)
107130
if not res:
108131
logger.info("Cancel clicked (confirm update)")
109-
raise self.abort("Abort: canceled by user (confirm update)")
132+
raise self.abort("canceled by user (confirm update)")
110133

111134
logger.info("OK clicked (confirm update)")
112135
self.common_ui.touch.start.emit()
@@ -122,30 +145,30 @@ def confirm_update_same_version(self, version: Version) -> None:
122145
)
123146
if not res:
124147
logger.info("Cancel clicked (confirm same version)")
125-
raise self.abort("Abort: canceled by user (confirm same version)")
148+
raise self.abort("canceled by user (confirm same version)")
126149

127150
logger.info("OK clicked (confirm same version)")
128151

129152
def confirm_extra_information(self, txt: List[str]) -> None:
130153
res = self.run_confirm_dialog("Confirm extra information", " ".join(txt))
131154
if not res:
132155
logger.info("Cancel clicked (confirm extra info)")
133-
raise self.abort("Abort: canceled by user (confirm extra info)")
156+
raise self.abort("canceled by user (confirm extra info)")
134157

135158
logger.info("OK clicked (confirm same version)")
136159

137160
def abort_pynitrokey_version(
138161
self, current: Version, required: Version
139162
) -> Exception:
140-
raise self.abort(f"Abort: pynitrokey {required} too old, need: {current}")
163+
raise self.abort(f"pynitrokey {required} too old, need: {current}")
141164

142165
def confirm_pynitrokey_version(self, current: Version, required: Version) -> None:
143166
# TODO: implement
144-
raise self.abort(f"Abort: pynitrokey {required} too old, need: {current}")
167+
raise self.abort(f"pynitrokey {required} too old, need: {current}")
145168

146169
def request_repeated_update(self) -> Exception:
147170
logger.info("Bootloader mode enabled. Repeat to update")
148-
return self.abort("Abort: bootloader enabled")
171+
return self.abort("bootloader enabled")
149172

150173
def request_bootloader_confirmation(self) -> None:
151174
logger.info("requesting bootloader confirmation")
@@ -173,15 +196,6 @@ def finalization_progress_bar(
173196
self.common_ui.progress.start.emit("Finalization")
174197
yield self.common_ui.progress.progress.emit
175198

176-
def _print_firmware_versions(
177-
self, current: Optional[Version], new: Optional[Version]
178-
) -> None:
179-
if not self._version_printed:
180-
current_str = str(current) if current else "[unknown]"
181-
logger.info(f"Current firmware version: {current_str}")
182-
logger.info(f"Updated firmware version: {new}")
183-
self._version_printed = True
184-
185199

186200
class Nk3Context(DeviceHandler):
187201
def __init__(self, path: str) -> None:
@@ -245,21 +259,31 @@ def update(
245259
self,
246260
ui: UpdateGUI,
247261
image: Optional[str] = None,
248-
version: Optional[str] = None,
249-
ignore_pynitrokey_version: bool = False,
250-
) -> None:
251-
with self.connect() as device:
252-
updater = Updater(ui, self)
253-
_, status = updater.update(
254-
device, image, version, ignore_pynitrokey_version
255-
)
262+
) -> UpdateResult:
263+
try:
264+
with self.connect() as device:
265+
updater = Updater(ui, self)
266+
_, status = updater.update(
267+
device=device, image=image, update_version=None
268+
)
269+
except UpdateException as e:
270+
return UpdateResult(status=e.status, message=str(e))
271+
except Exception as e:
272+
return UpdateResult(status=UpdateStatus.ERROR, message=str(e))
273+
256274
if status.init_status is not None:
257275
if status.init_status & InitStatus.EXT_FLASH_NEED_REFORMAT:
258-
raise Exception(
259-
"External filesystem needs to be reformatted."
260-
" Please contact support@nitrokey.com for more information on how to solve this issue."
276+
logger.error(
277+
f"Problematic init status after update: {status.init_status}"
278+
)
279+
return UpdateResult(
280+
status=UpdateStatus.ERROR,
281+
message="External filesystem needs to be reformatted."
282+
" Please contact support@nitrokey.com for more information on how to solve this issue.",
261283
)
262284

285+
return UpdateResult(status=UpdateStatus.SUCCESS)
286+
263287

264288
class Try:
265289
"""Utility class for an execution of a repeated action with Retries."""

0 commit comments

Comments
 (0)