From 27b3f4cab40305173a1ae6db8f3a82e72f017dc9 Mon Sep 17 00:00:00 2001 From: TraceLumen Date: Sat, 19 Sep 2026 23:10:41 +0300 Subject: [PATCH] v1.0.8 dualside work, have bugs/need work --- install.sh | 0 src/general/version.rs | 2 +- src/local/print/advance.rs | 54 ++++++++++++++++++++++++++++---------- src/local/print/create.rs | 37 ++++++++++++++++++++------ src/local/print/printer.rs | 22 ++++++---------- 5 files changed, 78 insertions(+), 37 deletions(-) mode change 100644 => 100755 install.sh diff --git a/install.sh b/install.sh old mode 100644 new mode 100755 diff --git a/src/general/version.rs b/src/general/version.rs index c0abfac..6b37cdf 100644 --- a/src/general/version.rs +++ b/src/general/version.rs @@ -2,7 +2,7 @@ use axum::{response::IntoResponse, Json}; pub async fn get_version() -> impl IntoResponse { Json(serde_json::json!({ - "version": "1.0.7", + "version": "1.0.8", "timestamp": chrono::Utc::now().timestamp() })) } diff --git a/src/local/print/advance.rs b/src/local/print/advance.rs index 105790d..43930ee 100644 --- a/src/local/print/advance.rs +++ b/src/local/print/advance.rs @@ -11,33 +11,57 @@ pub async fn job_advance( let job = m.get_mut(&job_id).ok_or_else(|| (StatusCode::NOT_FOUND, "Задание не найдено".into()))?; let si = match job.phase { - JobPhase::WaitingStart => { - let next_idx = 0; - job.current = Some(next_idx); - Some(start_printing_locked(job, next_idx)) + // Старт печати текущего файла (первого или после очистки) + JobPhase::WaitingStart | JobPhase::AwaitingClearOutput => { + let idx = job.current.unwrap_or(0); + Some(start_printing_locked(job, idx)) } + JobPhase::AwaitingPickup => { let c = job.current.unwrap_or(0); if job.duplex_first_pass_done { + // Первый проход двусторонней печати завершен -> просим переложить job.duplex_first_pass_done = false; - job.files[c].status = FileStatus::AwaitingClearOutput; - job.phase = JobPhase::AwaitingClearOutput; None + job.files[c].status = FileStatus::AwaitingFlip; + job.phase = JobPhase::AwaitingFlip; + None } else { + // Файл полностью напечатан job.files[c].status = FileStatus::Done; - begin_next_locked(job, c) + + // Ищем следующий файл + if let Some(next_si) = begin_next_locked(job, c) { + let next_idx = job.current.unwrap(); + + // Проверяем, является ли СЛЕДУЮЩИЙ файл двусторонним + let is_next_duplex = job.files[next_idx].settings.as_ref() + .map(|s| s.sides.as_deref() == Some("Двусторонняя")) + .unwrap_or(false); + + if is_next_duplex { + // Следующий двусторонний -> просим очистить лоток ПЕРЕД его стартом + // ВАЖНО: Мы НЕ спавним задачу печати здесь. + // Мы меняем статус и ждем вызова advance (кнопки или автостарта) + job.files[next_idx].status = FileStatus::AwaitingClearOutput; + job.phase = JobPhase::AwaitingClearOutput; + None + } else { + // Следующий односторонний -> печатаем сразу + Some(next_si) + } + } else { + None // Задание завершено + } } } - JobPhase::AwaitingClearOutput => { - let c = job.current.unwrap_or(0); - job.files[c].status = FileStatus::AwaitingFlip; - job.phase = JobPhase::AwaitingFlip; None - } + JobPhase::AwaitingFlip => { let c = job.current.unwrap(); job.files[c].status = FileStatus::Printing; job.phase = JobPhase::Printing; Some(SpawnInfo { - job_id: job.id.clone(), idx: c, + job_id: job.id.clone(), + idx: c, path: job.files[c].path.to_path_buf(), settings: job.files[c].settings.clone(), is_second_pass: true, @@ -48,6 +72,8 @@ pub async fn job_advance( (job.view(), si) }; - if let Some(si) = spawn_info { spawn_print_task(store.clone(), si); } + if let Some(si) = spawn_info { + spawn_print_task(store.clone(), si); + } Ok(Json(view)) } diff --git a/src/local/print/create.rs b/src/local/print/create.rs index 1c1383e..121c76c 100644 --- a/src/local/print/create.rs +++ b/src/local/print/create.rs @@ -10,6 +10,7 @@ pub async fn create_print( mut multipart: Multipart, ) -> Result, (StatusCode, String)> { info!("📥 Получен запрос на печать"); + let mut settings_list: Vec> = Vec::new(); let mut files_data: Vec<(String, bytes::Bytes)> = Vec::new(); @@ -23,35 +24,55 @@ pub async fn create_print( settings_list.push(parsed); } else if field_name == "files" { let file_name = field.file_name().unwrap_or("unknown").to_string(); - if let Ok(data) = field.bytes().await { files_data.push((file_name, data)); } + if let Ok(data) = field.bytes().await { + files_data.push((file_name, data)); + } } } - if files_data.is_empty() { return Err((StatusCode::BAD_REQUEST, "Нет файлов".into())); } + if files_data.is_empty() { + return Err((StatusCode::BAD_REQUEST, "Нет файлов".into())); + } let mut job_files: Vec = Vec::new(); for (i, (file_name, data)) in files_data.into_iter().enumerate() { let ext = Path::new(&file_name).extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase(); let temp = if ext.is_empty() { NamedTempFile::new() } else { NamedTempFile::with_suffix(&format!(".{}", ext)) }; - + if let Ok(temp) = temp { let path = temp.into_temp_path(); if fs::write(&*path, &data).await.is_ok() { let settings = settings_list.get(i).cloned().flatten(); job_files.push(JobFile { - name: file_name, path, settings, - status: FileStatus::Queued, error: None, + name: file_name, + path, + settings, + status: FileStatus::Queued, + error: None, }); } } } - if job_files.is_empty() { return Err((StatusCode::BAD_REQUEST, "Не удалось сохранить файлы".into())); } + if job_files.is_empty() { + return Err((StatusCode::BAD_REQUEST, "Не удалось сохранить файлы".into())); + } + + // ВСЕГДА начинаем с WaitingStart. + // 1. Если первый файл односторонний - он просто поедет печататься. + // 2. Если первый файл двусторонний - лоток и так пуст, очистка не нужна. + // Очистка (AwaitingClearOutput) будет запрошена ТОЛЬКО при переходе + // от предыдущего файла к следующему двустороннему в advance.rs + let initial_phase = JobPhase::WaitingStart; let job_id = format!("job-{}", JOB_COUNTER.fetch_add(1, Ordering::SeqCst)); let job = Job { - id: job_id.clone(), files: job_files, current: None, - phase: JobPhase::WaitingStart, cups_id: None, duplex_first_pass_done: false, + id: job_id.clone(), + files: job_files, + current: Some(0), + phase: initial_phase, + cups_id: None, + duplex_first_pass_done: false, }; let view = job.view(); diff --git a/src/local/print/printer.rs b/src/local/print/printer.rs index ff3786f..bf77d21 100644 --- a/src/local/print/printer.rs +++ b/src/local/print/printer.rs @@ -35,15 +35,15 @@ pub fn spawn_print_task(store: JobStore, info: SpawnInfo) { let is_duplex = info.settings.as_ref() .map(|s| s.sides.as_deref() == Some("Двусторонняя")) .unwrap_or(false); - + let duplex_pass: Option<&str> = if is_duplex { if info.is_second_pass { Some("even") } else { Some("odd") } } else { None }; - + let p = info.path.clone(); let s = info.settings.clone(); let dp = duplex_pass.map(|d| d.to_string()); - + let spool = tokio::task::spawn_blocking(move || run_lp(&p, s.as_ref(), dp.as_deref())).await; let cups_id = match spool { @@ -54,15 +54,14 @@ pub fn spawn_print_task(store: JobStore, info: SpawnInfo) { let mut m = store.lock().unwrap(); let job = match m.get_mut(&info.job_id) { Some(j) => j, None => return }; - + if job.phase == JobPhase::Cancelled { return; } if job.files.get(info.idx).map(|f| f.status.clone()) != Some(FileStatus::Printing) { return; } - - if !cups_id.is_empty() { job.cups_id = Some(cups_id); } + if !cups_id.is_empty() { job.cups_id = Some(cups_id); } job.files[info.idx].status = FileStatus::AwaitingPickup; job.phase = JobPhase::AwaitingPickup; - + if is_duplex && !info.is_second_pass { job.duplex_first_pass_done = true; } else { @@ -77,7 +76,6 @@ pub async fn mark_error_and_continue(store: JobStore, info: SpawnInfo, msg: Stri let mut m = store.lock().unwrap(); let job = match m.get_mut(&info.job_id) { Some(j) => j, None => return }; if job.phase == JobPhase::Cancelled { return; } - if let Some(f) = job.files.get_mut(info.idx) { if f.status != FileStatus::Printing { return; } f.status = FileStatus::Error; @@ -118,10 +116,8 @@ fn run_lp(path: &Path, settings: Option<&PrintSettings>, duplex_pass: Option<&st cmd.arg("-n").arg(s.copies.to_string()); if s.pages != "all" && !s.pages.is_empty() { cmd.arg("-P").arg(&s.pages); } if s.color_mode == "bw" { cmd.arg("-o").arg("ColorModel=Gray"); } - let fmt = normalize_format(&s.format); if !fmt.is_empty() { cmd.arg("-o").arg(format!("PageSize={}", fmt)); } - if s.sides.as_deref() == Some("Двусторонняя") && duplex_pass.is_none() { cmd.arg("-o").arg("sides=two-sided-long-edge"); } @@ -135,11 +131,11 @@ fn run_lp(path: &Path, settings: Option<&PrintSettings>, duplex_pass: Option<&st match duplex_pass { Some("odd") => { cmd.arg("-o").arg("InputSlot=Upper"); - cmd.arg("-o").arg("page-set=odd"); + cmd.arg("-o").arg("page-set=odd"); } Some("even") => { cmd.arg("-o").arg("InputSlot=Lower"); - cmd.arg("-o").arg("page-set=even"); + cmd.arg("-o").arg("page-set=even"); cmd.arg("-o").arg("outputorder=reverse"); cmd.arg("-o").arg("orientation-requested=6"); } @@ -148,10 +144,8 @@ fn run_lp(path: &Path, settings: Option<&PrintSettings>, duplex_pass: Option<&st cmd.arg(path); let out = cmd.env("LC_ALL", "C").output().map_err(|e| format!("lp: {}", e))?; - if !out.status.success() { return Err(String::from_utf8_lossy(&out.stderr).trim().to_string()); } - Ok(String::from_utf8_lossy(&out.stdout).split_whitespace().nth(3).unwrap_or("").to_string()) }