diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs index d3d74d1..740090b 100644 --- a/src/jobs/mod.rs +++ b/src/jobs/mod.rs @@ -11,6 +11,7 @@ //! UI'nin hemen güncellenmesi sağlanır. +use std::path::Path; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc; use tokio::io::{AsyncBufReadExt, BufReader}; @@ -140,6 +141,43 @@ impl Job for CopyFilesJob { fn name(&self) -> &str { "job_copy_files" } async fn run(&self, ui: &UiSender) -> Result<(), String> { + ui.log(format!("[DEBUG] CopyFilesJob başladı")); + ui.log(format!("[DEBUG] Kaynak: '{}'", self.source)); + ui.log(format!("[DEBUG] Hedef: '{}'", self.target)); + + // Kaynak dizini kontrol et + let source_path = std::path::Path::new(&self.source); + let source_exists = source_path.exists(); + let source_readable = source_exists && source_path.is_dir(); + ui.log(format!("[DEBUG] Kaynak mevcut: {}, dizin: {}", source_exists, source_readable)); + + if !source_exists { + ui.log(format!("[DEBUG] Kaynak dizin içeriği aranıyor...")); + // Kaynağın bir üst dizinini dene + let parent = source_path.parent(); + if let Some(p) = parent { + if p.exists() { + if let Ok(entries) = std::fs::read_dir(p) { + for entry in entries.flatten().take(20) { + ui.log(format!("[DEBUG] /{:?}", entry.path())); + } + } + } + } else { + ui.log("[DEBUG] Kaynağın parent'ı yok (kök dizin olabilir)"); + } + } else if source_readable { + // İlk 10 girdiyi listele + if let Ok(entries) = std::fs::read_dir(source_path) { + let mut count = 0; + for entry in entries.flatten().take(10) { + ui.log(format!("[DEBUG] {:?}", entry.path())); + count += 1; + } + ui.log(format!("[DEBUG] (ilk {} girdi listelendi)", count)); + } + } + ui.log(format!("rsync {} → {}", self.source, self.target)); let is_root_source = self.source.trim_end_matches('/') == ""; @@ -204,53 +242,113 @@ impl Job for CopyFilesJob { return Ok(()); } - let mut child = Command::new("rsync") - .args(&args) + // rsync çıktısı pipe'a yönlendirilince tam tamponlu (fully buffered) olur, + // bu da progress çıktısının gerçek zamanlı gelmemesine yol açar. + // stdbuf -eL ile stderr'i satır tamponlu (line-buffered) yapıyoruz. + let (program, final_args) = if Path::new("/usr/bin/stdbuf").exists() { + let mut a = vec!["-eL".to_string(), "rsync".to_string()]; + a.extend(args); + ("stdbuf", a) + } else { + ("rsync", args) + }; + + let mut child = Command::new(program) + .args(&final_args) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .map_err(|e| format!("rsync çalıştırılamadı: {}", e))?; + let stdout = child.stdout.take() + .ok_or_else(|| "rsync stdout alınamadı".to_string())?; let stderr = child.stderr.take() .ok_or_else(|| "rsync stderr alınamadı".to_string())?; - let reader = BufReader::new(stderr); + + // Stderr'i arka planda okuyarak tamponun dolmasını ve rsync'in kilitlenmesini engelliyoruz. + let stderr_err_accumulator = std::sync::Arc::new(tokio::sync::Mutex::new(String::new())); + let stderr_accumulator_clone = stderr_err_accumulator.clone(); + tokio::spawn(async move { + let reader = BufReader::new(stderr); + let mut lines = reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + let mut accum = stderr_accumulator_clone.lock().await; + accum.push_str(&line); + accum.push('\n'); + } + }); + + let reader = BufReader::new(stdout); let mut lines = reader.lines(); while let Some(line) = lines.next_line().await .map_err(|e| format!("rsync çıktısı okunamadı: {}", e))? { - let trimmed = line.trim(); - // rsync --info=progress2 format (newlines when piped): - // 1,024 100% 512.00kB/s 0:00:00 (xfr#5, to-chk=99/105) - // Extract xfr# and to-chk for sub-progress - if let Some(xfr_pos) = trimmed.find("xfr#") { - let after_xfr = &trimmed[xfr_pos + 4..]; - let xfr_end = after_xfr.find(|c: char| !c.is_ascii_digit()).unwrap_or(after_xfr.len()); - let xfr: u64 = after_xfr[..xfr_end].parse().unwrap_or(0); + // Pipe'a yönlendirilince rsync satırbaşı (\r) içerebilir. + // Önce \r karakterlerini temizle, sonra trim yap. + let cleaned: String = line.chars() + .filter(|&c| c != '\r') + .collect(); + let trimmed = cleaned.trim().to_string(); - if let Some(to_chk_pos) = trimmed.find("to-chk=") { - let after_to = &trimmed[to_chk_pos + 7..]; - let slash_pos = after_to.find('/').unwrap_or(after_to.len()); - let total_end = after_to[slash_pos + 1..] - .find(|c: char| !c.is_ascii_digit()) - .unwrap_or(after_to[slash_pos + 1..].len()); - let total: u64 = after_to[slash_pos + 1..][..total_end].parse().unwrap_or(1); + if trimmed.is_empty() { + continue; + } - let p = if total > 0 { - (xfr as f32) / (total as f32) - } else { - 0.0 - }; + // ── 1) Progress satırı mı? ────────────────────────── + // rsync --info=progress2 format (pipe yoluyla): + // " 1,024 100% 512.00kB/s 0:00:00 (xfr#5, to-chk=99/105)" + // " 3.456M 67% 5.12MB/s 0:00:01 (xfr#17, to-chk=83/105)" + // (xfr# ve to-chk= varsa progress satırıdır) + if trimmed.contains("xfr#") && trimmed.contains("to-chk=") { + // xfr# değerini çıkar + let xfr = if let Some(pos) = trimmed.find("xfr#") { + let after = &trimmed[pos + 4..]; + let end = after.find(|c: char| !c.is_ascii_digit()).unwrap_or(after.len()); + after[..end].parse::().unwrap_or(0) + } else { 0 }; + + // to-chk= içinden total değerini çıkar + let total = if let Some(pos) = trimmed.find("to-chk=") { + let after = &trimmed[pos + 7..]; + let slash_pos = after.find('/'); + if let Some(sp) = slash_pos { + let after_slash = &after[sp + 1..]; + let end = after_slash.find(|c: char| !c.is_ascii_digit()).unwrap_or(after_slash.len()); + after_slash[..end].parse::().unwrap_or(1) + } else { 1 } + } else { 1 }; + + if total > 0 { + let p = (xfr as f32) / (total as f32); ui.sub_progress(p.min(1.0)); } + continue; + } + + // ── 2) Progress satırının ilk kısmı mı? (sayı + yüzde ile başlayan) ── + // rsync pipe çıktısı bazen iki satıra bölünebilir: + // " 1,024 100% 512.00kB/s 0:00:00" + // " (xfr#5, to-chk=99/105)" + // Bu durumda ilk satırda sadece sayı/yüzde/hız bilgisi vardır, atla. + { + let first = trimmed.chars().next().unwrap_or(' '); + if first.is_ascii_digit() || first == ' ' { + // Sayı veya boşlukla başlayıp xfr içermiyorsa progress'in devamıdır + continue; + } } - // Extract filename: lines starting with a path (not a number/space) - let first_char = trimmed.chars().next(); - if let Some('/') = first_char { - ui.current_file(trimmed.to_string()); - } else if trimmed.starts_with("./") || trimmed.starts_with("..") { - ui.current_file(trimmed.to_string()); + // ── 3) Gerçek dosya adı ────────────────────────────── + // rsync --out-format kullanılmadığında dosya adları da stderr'e + // göreceli yol olarak yazılır: "etc/passwd", "usr/share/doc/..." + // Bunlar genellikle '.' '/' veya harf ile başlar, sayı ile başlamaz. + // progress satırı olmadığına göre bu bir dosya adıdır. + if trimmed.len() > 3 && !trimmed.starts_with('[') && !trimmed.contains("sending incremental") + && !trimmed.contains("building file list") && !trimmed.contains("done") + && !trimmed.contains("sent ") && !trimmed.contains("total size") + { + ui.current_file(trimmed); } } @@ -258,7 +356,12 @@ impl Job for CopyFilesJob { .map_err(|e| format!("rsync beklenemedi: {}", e))?; if !status.success() { - return Err(format!("rsync çıkış kodu: {:?}", status.code())); + let stderr_content = stderr_err_accumulator.lock().await.clone(); + return Err(format!( + "rsync çıkış kodu: {:?}. Hata: {}", + status.code(), + stderr_content.trim() + )); } Ok(()) diff --git a/src/steps/execution.rs b/src/steps/execution.rs index ad8d11d..6663d4b 100644 --- a/src/steps/execution.rs +++ b/src/steps/execution.rs @@ -178,6 +178,10 @@ impl InstallerStep for ExecutionStep { loop { match rx.try_recv() { Ok(InstallMessage::Log(line)) => { + if line.starts_with('▶') { + state.install_sub_progress = 0.0; + state.install_current_file = None; + } state.install_log.push(line); } Ok(InstallMessage::Progress(p)) => { @@ -268,8 +272,7 @@ impl InstallerStep for ExecutionStep { // İlerleme çubuğu let running = !self.finished - && self.error_screen.is_none() - && state.install_progress > 0.0; + && self.error_screen.is_none(); egui::Frame::none() .fill(theme::c_bg_widget()) @@ -313,15 +316,13 @@ impl InstallerStep for ExecutionStep { .desired_height(10.0), ); - if state.install_sub_progress > 0.0 { - ui.add_space(4.0); - ui.add( - egui::ProgressBar::new(state.install_sub_progress) - .animate(running) - .desired_height(6.0) - .fill(theme::c_accent_dim()), - ); - } + ui.add_space(4.0); + ui.add( + egui::ProgressBar::new(state.install_sub_progress) + .animate(running) + .desired_height(6.0) + .fill(theme::c_accent_dim()), + ); }); // Durum etiketi