Пытаемся угомонить превьюшки
All checks were successful
Deploy MES Core / deploy (push) Successful in 10s

This commit is contained in:
2026-04-03 01:58:23 +03:00
parent b76ce4913f
commit 1fe05d41f6
5 changed files with 141 additions and 39 deletions

View File

@@ -4,6 +4,9 @@ from urllib.parse import urlsplit
import os
import subprocess
import sys
from pathlib import Path
from django.conf import settings as django_settings
from django.contrib import messages
from django.core.files.base import ContentFile
@@ -169,7 +172,12 @@ def _update_task_preview(task: ProductionTask) -> bool:
name = (task.drawing_file.name or '').lower()
if not name.endswith('.dxf'):
# Если не DXF — превью не делаем и очищаем габариты
# Если не DXF — превью не делаем: удаляем старый PNG (если был) и очищаем габариты.
try:
if task.preview_image:
task.preview_image.delete(save=False)
except Exception:
pass
task.preview_image = None
task.blank_dimensions = ''
task.save(update_fields=['preview_image', 'blank_dimensions'])
@@ -184,16 +192,20 @@ def _update_task_preview(task: ProductionTask) -> bool:
min_lineweight_mm=settings.min_lineweight,
keep_original_colors=settings.keep_original_colors,
)
dims = ''
# Временно отключаем вычисление габаритов (bbox), чтобы исключить его влияние на стабильность.
# Превью PNG генерируем как и раньше.
# Перед сохранением удаляем старое превью, иначе FileSystemStorage добавляет суффиксы
# и папка с превью постепенно «засоряется».
try:
dims = _extract_dxf_dimensions(dxf_path)
if task.preview_image:
task.preview_image.delete(save=False)
except Exception:
dims = ''
pass
filename = f"task_{task.id}_preview.png"
task.preview_image.save(filename, ContentFile(png_bytes), save=False)
task.blank_dimensions = dims
task.save(update_fields=['preview_image', 'blank_dimensions'])
task.save(update_fields=['preview_image'])
return True
# Класс главной страницы (роутер)
@@ -644,6 +656,34 @@ def _mark_stale_preview_jobs() -> None:
job.save(update_fields=['status', 'finished_at', 'last_message'])
def _dxf_job_log_path(job_id: int) -> Path:
"""Путь к лог-файлу фоновой задачи DXF превью."""
base_dir = Path(getattr(django_settings, 'BASE_DIR', Path(__file__).resolve().parent.parent))
logs_dir = base_dir / 'logs'
logs_dir.mkdir(parents=True, exist_ok=True)
return logs_dir / f'dxf_preview_job_{job_id}.log'
def _read_tail(path: Path, max_bytes: int = 32_000) -> str:
"""Читает «хвост» файла (последние max_bytes) для вывода в UI."""
try:
if not path.exists():
return ''
with path.open('rb') as f:
f.seek(0, os.SEEK_END)
size = f.tell()
start = max(0, size - max_bytes)
f.seek(start)
data = f.read()
if start > 0:
nl = data.find(b'\n')
if nl != -1:
data = data[nl + 1 :]
return data.decode('utf-8', errors='replace')
except Exception:
return ''
class MaintenanceStatusView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
profile = getattr(request.user, 'profile', None)
@@ -651,12 +691,13 @@ class MaintenanceStatusView(LoginRequiredMixin, View):
if role != 'admin':
return JsonResponse({'error': 'forbidden'}, status=403)
# Перед отдачей статуса пытаемся снять «залипшие» задачи.
_mark_stale_preview_jobs()
job = DxfPreviewJob.objects.order_by('-id').first()
if not job:
return JsonResponse({'job': None})
return JsonResponse({'job': None, 'log_tail': ''})
log_tail = _read_tail(_dxf_job_log_path(job.id))
return JsonResponse({
'job': {
@@ -668,8 +709,10 @@ class MaintenanceStatusView(LoginRequiredMixin, View):
'updated': job.updated,
'skipped': job.skipped,
'errors': job.errors,
'cancel_requested': job.cancel_requested,
'cancel_requested': getattr(job, 'cancel_requested', False),
'last_message': job.last_message,
'log_tail': log_tail,
'log_path': str(_dxf_job_log_path(job.id)),
}
})
@@ -739,6 +782,25 @@ class MaintenanceView(LoginRequiredMixin, TemplateView):
messages.success(request, 'Остановка запрошена.')
return redirect('maintenance')
if action == 'clear_log':
# Очистка лог-файла последней задачи. Во время выполнения не трогаем,
# потому что процесс может держать открытый дескриптор файла.
job = DxfPreviewJob.objects.order_by('-id').first()
if not job:
messages.info(request, 'Логов нет.')
return redirect('maintenance')
if job.status in ['queued', 'running'] and not job.finished_at:
messages.warning(request, 'Нельзя очистить лог во время выполнения задачи.')
return redirect('maintenance')
try:
_dxf_job_log_path(job.id).open('wb').close()
messages.success(request, 'Лог очищен.')
except Exception:
messages.error(request, 'Не удалось очистить лог.')
return redirect('maintenance')
if action != 'update_previews':
messages.success(request, 'Настройки превью сохранены.')
return redirect('maintenance')
@@ -758,14 +820,27 @@ class MaintenanceView(LoginRequiredMixin, TemplateView):
job = DxfPreviewJob.objects.create(status='queued', created_by=request.user)
try:
subprocess.Popen(
[sys.executable, 'manage.py', 'dxf_preview_job', str(job.id)],
cwd=os.path.dirname(os.path.dirname(__file__)),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
close_fds=True,
)
messages.success(request, 'Запущено обновление превью DXF в фоне. Прогресс обновляется ниже.')
log_path = _dxf_job_log_path(job.id)
log_fh = log_path.open('ab')
try:
p = subprocess.Popen(
[sys.executable, 'manage.py', 'dxf_preview_job', str(job.id)],
cwd=str(Path(__file__).resolve().parent.parent),
stdout=log_fh,
stderr=log_fh,
close_fds=True,
)
finally:
log_fh.close()
# Если в модели есть поле pid — сохраняем его для диагностики.
try:
job.pid = p.pid
job.save(update_fields=['pid'])
except Exception:
pass
messages.success(request, 'Запущено обновление превью DXF в фоне. Прогресс и лог обновляются ниже.')
except Exception:
job.status = 'failed'
job.last_message = 'Не удалось запустить фоновый процесс генерации превью.'