@@ -1671,7 +1671,6 @@ class TaskStatusView(TemplateView):
return context
def task_log_view(request, task_id):
- from celery.result import AsyncResult
if not request.user.is_authenticated:
raise PermissionDenied
@@ -1679,7 +1678,11 @@ def task_log_view(request, task_id):
# Block anything that looks like a path
raise Http404
- result = AsyncResult(task_id)
+ # The task records when it finished and its result in the Update. Read
+ # that before the log, which is complete by the time it is set. (The
+ # Celery result backend is no use here: with rpc:// only the process
+ # that started the task can read the result.)
+ updateobj = get_object_or_404(Update, task_id=task_id)
start = int(request.GET.get('start', 0))
try:
f = open(os.path.join(settings.TASK_LOG_DIR, 'task_%s.log' % task_id), 'rb')
@@ -1693,24 +1696,13 @@ def task_log_view(request, task_id):
datastr = re.sub(b'\n[^\n]+\r', b'\n', datastr)
# We need to escape this or else things that look like tags in the output
# will be interpreted as such by the browser
- data = escape(datastr)
+ data = escape(datastr.decode('utf-8', errors='replace'))
response = HttpResponse(data)
- try:
- ready = result.ready()
- except ConnectionResetError:
- # FIXME this shouldn't be happening so often, but ultimately we don't care -
- # the frontend is polling so it'll likely succeed in a subsequent request
- ready = False
- if ready:
+ if updateobj.finished:
response['Task-Done'] = '1'
- updateobj = get_object_or_404(Update, task_id=task_id)
response['Task-Duration'] = utils.timesince2(updateobj.started, updateobj.finished)
response['Task-Progress'] = 100
- if result.info:
- if isinstance(result.info, dict):
- response['Task-Result'] = result.info.get('retcode', None)
- else:
- response['Task-Result'] = -1
+ response['Task-Result'] = updateobj.retcode
else:
response['Task-Done'] = '0'
preader = utils.ProgressReader(settings.TASK_LOG_DIR, task_id)
@@ -60,3 +60,25 @@ def test_run_update_command_keeps_fields_set_by_command(settings, tmp_path, monk
assert update.reload
assert (update.log, update.retcode) == ('output', 0)
assert update.finished is not None
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize('finished, retcode, done, result', [
+ (False, 0, '0', None),
+ (True, 0, '1', '0'),
+ (True, 2, '1', '2'),
+])
+def test_task_log_reports_status_from_update(client, django_user_model, settings, tmp_path,
+ finished, retcode, done, result):
+ settings.TASK_LOG_DIR = str(tmp_path)
+ (tmp_path / 'task_test-task.log').write_text('line 1 <b>\n', encoding='utf-8')
+ Update.objects.create(started=datetime.now(), task_id='test-task', retcode=retcode,
+ finished=datetime.now() if finished else None)
+ client.force_login(django_user_model.objects.create_user('user', 'user@example.com', 'pw'))
+
+ response = client.get(reverse('task_log', args=('test-task',)))
+
+ assert response.status_code == 200
+ assert response.content == b'line 1 <b>\n'
+ assert response['Task-Done'] == done
+ assert response.get('Task-Result') == result
The task status page showed the log as a Python bytes literal, because escape() no longer decodes bytes since Django 3.0, and did not see the task finish when the poll reached another process than the one that started it, because the rpc:// result backend only returns results to that process. Decode the log, and take whether the task finished and its exit status from the update record, which the task already sets. [YOCTO #15395] AI-Generated: Claude Opus 5.5 Signed-off-by: Tim Orling <tim.orling@konsulko.com> --- layerindex/views.py | 24 ++++++++---------------- tests/test_update_button.py | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+), 16 deletions(-)