aboutsummaryrefslogtreecommitdiff
path: root/app/ui/runview.py
diff options
context:
space:
mode:
Diffstat (limited to 'app/ui/runview.py')
-rw-r--r--app/ui/runview.py55
1 files changed, 25 insertions, 30 deletions
diff --git a/app/ui/runview.py b/app/ui/runview.py
index 21ac08c..005db59 100644
--- a/app/ui/runview.py
+++ b/app/ui/runview.py
@@ -37,6 +37,7 @@ from typing import Callable, List, Optional
from backends import common, servers
from ui import tui
+from ui import viewkit
from ui.viewkit import (TERMINAL_PHASES as _TERMINAL,
DRAW_TIMEOUT_MS as _DRAW_TIMEOUT_MS,
ScreenView, _box, _fit, _format_elapsed, _sep,
@@ -58,42 +59,23 @@ _SERVER_STATES = {
_MONITOR_INTERVAL = 2.0
-class _LogAppender:
+class _LogAppender(viewkit.LineSplitter):
"""A file-like that appends redirected console output to the run's log.
The run view owns the screen, so anything a conversion prints to
stdout/stderr outside the progress events would otherwise be swallowed
- silently; this mirrors it line by line into the run's dated log file
+ silently; this mirrors it line by line (\\n and \\r — see
+ viewkit.LineSplitter) into the run's dated log file
(RunConfig.log_path, the audiobook_ day stream), prefixed with the same
timestamp format the converter's log records use. Best-effort: write
errors are swallowed, and an empty path disables logging.
"""
def __init__(self, path: str):
+ super().__init__(self._append_line)
self._path = path
- self._buffer = ""
-
- def write(self, text: str) -> int:
- if not text:
- return 0
- self._buffer += text
- while True:
- cut = self._buffer.find("\n")
- if cut < 0:
- break
- line, self._buffer = self._buffer[:cut], self._buffer[cut + 1:]
- self._append(line)
- return len(text)
-
- def flush(self) -> None:
- if self._buffer:
- self._append(self._buffer)
- self._buffer = ""
-
- def isatty(self) -> bool:
- return False
- def _append(self, line: str) -> None:
+ def _append_line(self, line: str) -> None:
if not self._path or not line.strip():
return
try:
@@ -274,6 +256,10 @@ class RunView(ScreenView):
if event.get("cancelled"):
self.cancelled = True
self._finish("cancelled")
+ elif total == 0 and ok == 0 and not self.error_message:
+ # An empty run (no books found, or all skipped): a clean
+ # no-op, not a failure — there was nothing that could fail.
+ self._finish("done")
elif total and ok >= total and not self.error_message:
self._finish("done")
else:
@@ -404,21 +390,30 @@ class RunView(ScreenView):
going.
"""
self._blocking()
- answer = tui.confirm(self.scr, "Cancel processing?", default=False,
- cancel_value=False)
- if not answer:
+ try:
+ answer = tui.confirm(self.scr, "Cancel processing?", default=False,
+ cancel_value=False)
+ finally:
self._nonblocking()
+ if not answer:
return False
self.cancelling = True
self._cancel.set()
# Wind the worker down BEFORE offering the server stop: killing the
# server under a still-running request turns the cancellation into
# request failures (reported as "failed" instead of "cancelled").
- self._worker.join(timeout=60)
+ # The join is best-effort — a wedged worker delays but cannot veto
+ # the flow below.
+ self._join_worker()
# When this run booted the server, offer to shut it down too (the
# boot path kills it itself when cancelled before ready); by now
- # the worker is done, so nothing is mid-request.
- self._confirm_stop_server()
+ # the worker is done (or wedged beyond saving), so nothing further
+ # is mid-request from this view's side.
+ self._blocking()
+ try:
+ self._confirm_stop_server()
+ finally:
+ self._nonblocking()
self._drain()
self.render()
# One more key press acknowledges the final screen.