Пытаемся угомонить превьюшки
All checks were successful
Deploy MES Core / deploy (push) Successful in 10s
All checks were successful
Deploy MES Core / deploy (push) Successful in 10s
This commit is contained in:
@@ -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 = 'Не удалось запустить фоновый процесс генерации превью.'
|
||||
|
||||
Reference in New Issue
Block a user