diff --git a/src/main.rs b/src/main.rs index b1a30db..82cd110 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,7 @@ use tracing::{error, info, warn}; async fn get_version() -> impl IntoResponse { Json(serde_json::json!({ - "version": "1.0.3", + "version": "1.0.4", "timestamp": chrono::Utc::now().timestamp() })) } @@ -376,7 +376,7 @@ async fn job_cancel( for id in cups_ids { let cancel_bin = cups_bin("cancel"); let _ = tokio::task::spawn_blocking(move || { - Command::new(cancel_bin).arg(id).output() + Command::new(cancel_bin).arg(id).env("LC_ALL", "C").output() }) .await; } @@ -571,6 +571,22 @@ fn run_lp(path: &Path, settings: Option<&PrintSettings>, duplex_pass: Option<&st } } + // Выбор лотка (InputSlot) + if let Some(pass) = duplex_pass { + if pass == "even" { + // Второй проход (чётные страницы) — ручная подача + cmd.arg("-o").arg("InputSlot=Manual"); + // Поворот на 180° для второго прохода + cmd.arg("-o").arg("orientation-requested=6"); + } else { + // Первый проход (нечётные страницы) — нижний лоток + cmd.arg("-o").arg("InputSlot=Lower"); + } + } else { + // Обычная печать — нижний лоток + cmd.arg("-o").arg("InputSlot=Lower"); + } + // Ручной дуплекс: разбивка на чёт/нечёт match duplex_pass { Some("odd") => { @@ -588,7 +604,8 @@ fn run_lp(path: &Path, settings: Option<&PrintSettings>, duplex_pass: Option<&st let args: Vec = cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect(); info!(command = format!("{} {}", lp.display(), args.join(" ")), "🖨️ Команда печати"); - let out = cmd.output().map_err(|e| format!("Не удалось запустить lp: {}", e))?; + // ВАЖНО: LC_ALL=C гарантирует, что ответ будет на английском и парсинг ID не сломается + 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()); } @@ -596,18 +613,6 @@ fn run_lp(path: &Path, settings: Option<&PrintSettings>, duplex_pass: Option<&st Ok(stdout.split_whitespace().nth(3).unwrap_or("").to_string()) } -fn lpstat_raw(which: &str) -> Option { - let lpstat = cups_bin("lpstat"); - Command::new(&lpstat) - .arg("-W") - .arg(which) - .arg("-o") - .output() - .ok() - .filter(|o| o.status.success()) - .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) -} - fn has_job_in(output: &Option, id: &str) -> bool { output .as_deref() @@ -618,15 +623,25 @@ fn has_job_in(output: &Option, id: &str) -> bool { .unwrap_or(false) } -/// Ждём, пока CUPS-job исчезнет из не-завершённых. -/// «Не завершён» = всё, что в состоянии queued / active / held (принтер не ready). +fn lpstat_raw(which: &str) -> Option { + let lpstat = cups_bin("lpstat"); + Command::new(&lpstat) + .arg("-W") + .arg(which) + .arg("-o") + .env("LC_ALL", "C") + .output() + .ok() + .filter(|o| o.status.success()) + .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) +} + fn wait_for_cups(id: &str) -> Result<(), String> { let lpstat_path = cups_bin("lpstat"); - // Если lpstat физически отсутствует — не можем отслеживать, возвращаем ошибку if !lpstat_path.exists() && lpstat_path.to_str().unwrap_or("") == "lpstat" { - // пробуем найти через which if Command::new("which") .arg("lpstat") + .env("LC_ALL", "C") .output() .map(|o| !o.status.success()) .unwrap_or(true) @@ -640,22 +655,26 @@ fn wait_for_cups(id: &str) -> Result<(), String> { let started = Instant::now(); let timeout = Duration::from_secs(30 * 60); // 30 минут максимум - // Первый опрос: проверяем что lpstat вообще работает let probe = lpstat_raw("not-completed"); if probe.is_none() { return Err("lpstat не отвечает (возможно, не запущен cupsd)".into()); } + // Получаем имя принтера по умолчанию для проверки физического статуса + let printer_name = get_default_printer().unwrap_or_else(|| "Auto_HP_P2015".to_string()); + loop { - // "not-completed" = все задания, которые ещё не finished/cancelled - // Сюда попадают: pending, held, processing, а также "not ready" задания let not_completed = lpstat_raw("not-completed"); let in_queue = has_job_in(¬_completed, id); if !in_queue { - // Либо допечаталось (в completed), либо отменили — в любом случае наша работа сделана - info!(cups_id = %id, "✔ CUPS-задание покинуло очередь (допечатано/отменено)"); - return Ok(()); + // Задачи уже нет в очереди CUPS. Проверяем, допечатал ли принтер физически. + if !is_printer_physically_printing(&printer_name) { + info!(cups_id = %id, "✔ CUPS-задание выполнено и принтер физически допечатал"); + return Ok(()); + } else { + info!(cups_id = %id, "⏳ CUPS передал данные, но принтер всё ещё физически печатает из буфера..."); + } } if started.elapsed() > timeout { @@ -665,3 +684,30 @@ fn wait_for_cups(id: &str) -> Result<(), String> { std::thread::sleep(Duration::from_millis(1500)); } } + +fn get_default_printer() -> Option { + let out = Command::new("lpstat") + .arg("-d") + .env("LC_ALL", "C") + .output() + .ok()?; + let stdout = String::from_utf8_lossy(&out.stdout); + // Формат: "system default destination: Auto_HP_P2015" + stdout.split(':').nth(1).map(|s| s.trim().to_string()) +} + +fn is_printer_physically_printing(printer_name: &str) -> bool { + if let Ok(out) = Command::new("lpstat") + .arg("-p") + .arg(printer_name) + .env("LC_ALL", "C") + .output() + { + let stdout = String::from_utf8_lossy(&out.stdout); + // "now printing" или "processing" означают, что принтер физически печатает из буфера + if stdout.contains("now printing") || stdout.contains("processing") { + return true; + } + } + false +}