forked from pisilinux-rs/yali-rs
fix(P0): parola sızıntısı, parted birim hatası, hatalı LUKS cmdline, yutulan hatalar, hata temizliği
Veri kaybı / güvenlik / açılmayan sistem düzeyindeki 7 sorun giderildi.
## 1. Parola düz metin olarak argv'ye geçiyordu (jobs/users.rs)
`sh -c "echo 'user:pass' | chpasswd -c SHA512"` deseni kaldırıldı:
- parola /proc/<pid>/cmdline üzerinden okunabiliyordu
- ' veya $(...) içeren parola komutu bozuyordu
Doğru desen (`set_password_via_chpasswd`, stdin pipe) geri getirildi —
jobs/partition.rs::run_luks_format ile aynı. Yazılan veri yazımdan
sonra sıfırlanıyor. Dosyadaki yorum satırındaki güvenli sürüm de
silinerek tek uygulama bırakıldı.
## 2. parted "MB" birim tuzağı geri gelmişti (jobs/partition.rs:237)
EFI ve swap `{}MiB` geçerken kök bölüm `{}MB` idi. parted "MB"yi ondalık
(10^6) okur, bizim hesaplar ikilidir (2^20): 4609MB = 9001992.1875
sektör → hizasız başlangıç. AGENTS.md bunu KRİTİK not almıştı;
`7209d58` ile düzeltilmiş, sonra regresyona uğramıştı. `{}MiB` yapıldı
ve nedeni yorumla sabitlendi.
## 3. LUKS çekirdek parametresi geçersizdi (jobs/bootloader.rs)
`cryptdevice=UUID=<x>:cryptroot` geçerli bir söz dizimi değil, üstelik
kök bölümü `is_uefi` ile tahmin ediliyordu (UEFI'de 3, BIOS'ta 2) —
manuel modda bu numaralar assign_partition_numbers'tan geldiği için
ilgisiz bir bölümün UUID'si okunabiliyordu. `.unwrap_or_default()`
`UUID=:` üretebiliyordu.
Artık `cryptsetup status` ile mapper'ın arkasındaki GERÇEK aygıt
çalışma zamanında çözülüyor, blkid ile LUKS başlık UUID'si alınıyor,
ve dracut (`rd.luks.uuid=luks-…`) + mkinitcpio (`cryptdevice=…`)
biçimleri birlikte yazılıyor. UUID boş çıkarsa hata verilir; tahmin
yok.
Ayrıca `CRYPTROOT_NAME` sabiti tanımlandı: "cryptroot" 5 yerde
hardcoded'dı, artık tek kaynaktan geliyor.
## 4. Kritik hatalar yutuluyordu (jobs/finish.rs)
- RunComarJob tamamen yorum satırıydı → `pisi configure-pending`
hiç çalışmıyordu, paketlerin post-install betikleri koşmuyordu.
Geri getirildi; "yapılandırılacak paket yok" durumu zararsız sayılıyor.
- ldconfig ve update-environment hataları loglanıp yutuluyordu.
update-environment PisiLinux init finalizasyonudur: yutulursa
kurulum "başarılı" görünür ama sistem açılmaz. Artık fatal.
- CleanupLiveJob'da 10 adet `let _ =`; depo tanımları sessizce
başarısız olabiliyordu (kullanıcı paket kuramaz). Artık fatal.
## 5. Hata durumunda hiç temizlik yapılmıyordu (jobs/mod.rs)
run_all ilk hatada return ediyordu: hedef bölümler bağlı, LUKS
mapper'ları açık, swap aktif kalıyordu. Yeniden başlatma reddediliyor,
elle düzeltmesi dakikalar sürüyordu.
- JobQueue::cleanup listesi eklendi; with_cleanup(...) ile verilen
işler başarıda da hatada da çalışır.
- Yeni EmergencyCleanupJob (jobs/cleanup.rs): tüm mount'lar → LVM
lvchange -an → cryptsetup close (yalnızca hedef diskin
mapper'ları) → swapoff.
- Temizlik de hata verirse bu hatalar asıl hataya eklenip
kullanıcıya "elle ne yapılacak" listesi olarak gösterilir.
## 6. UnmountBindJob /home, /var, /boot'u çözmüyordu (jobs/execution.rs)
Elle yazılmış sabit liste (proc, sys, dev, boot/efi) yalnızca otomatik
modda doğruydu; manuel düzende ek bölümler kurulum bitince bağlı
kalıyordu. Artık /proc/self/mountinfo okunuyor ve mount/ altındaki her
şey en derinden yüzeye çözülüyor — düzen bilinmeye gerek yok.
mountinfo'daki \0NNN oktal kaçışlarını çözen unescape_mount_field ve
mounted_paths_under eklendi.
## 7. Parolasız LUKS kombinasyonu (jobs/partition.rs)
encrypt_root=true + boş parola → kök düz ext4 formatlanıyor, sonra
MountPartitionsJob /dev/mapper/cryptroot'u mount etmeye çalışıp
kurulumu öldürüyordu. Job başında reddediliyor.
## 8. rescue.rs'te iki panik noktası
- &entry.summary[..50]: len() bayt sayar, dilim bayt sınırı ister.
pisi history çıktısı paket adlarından gelir ve Türkçe karakter
içerir (ş/ğ 2 bayt) → sınırda PANİK. chars().take(50) ile değişti.
- parse_pisi_history: '[' ve ']' bağımsız aranıyordu; ']' önce
gelen girdide remainder[start+1..end] PANİK veriyordu. Kapanış
parantez açılıştan sonra aranıyor.
Her ikisi için 5 test eklendi (rescue.rs'te test modu yoktu).
## Doğrulama
cargo check --all-targets : 0 hata, 0 uyarı
cargo test : 28/28 geçti (23 + 5 yeni)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+75
-5
@@ -1,6 +1,52 @@
|
|||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use super::{blkid_uuid, DEMO_MODE, get_fs_type, is_mount_point, part_path, run_cmd, run_chroot, Job, UiSender};
|
use super::{blkid_uuid, DEMO_MODE, get_fs_type, is_mount_point, part_path, run_cmd, run_chroot, Job, UiSender};
|
||||||
|
|
||||||
|
/// Kök bölümün LUKS mapper adı. `PartitionJob`, `MountPartitionsJob`,
|
||||||
|
/// `GenerateFstabJob` ve `InstallGrubJob` aynı adı kullanır; tek
|
||||||
|
/// kaynaktan okunsun diye burada tanımlı.
|
||||||
|
pub const CRYPTROOT_NAME: &str = "cryptroot";
|
||||||
|
|
||||||
|
/// Açık bir LUKS mapper'ının arkasındaki GERÇEK aygıtı döndürür
|
||||||
|
/// (`/dev/sda3` gibi).
|
||||||
|
///
|
||||||
|
/// Neden gerekli: `blkid /dev/mapper/cryptroot` dosya sisteminin UUID'sini
|
||||||
|
/// verir, LUKS başlık UUID'sini değil. Kernel'e `rd.luks.uuid=` ile
|
||||||
|
/// verilmesi gereken değer LUKS başlığınkidir; bu yüzden önce mapper'ın
|
||||||
|
/// altındaki bölüme inmek gerekir. `cryptsetup status` tam olarak bu
|
||||||
|
/// bilgiyi verir ve çalışma zamanında doğrudur — bölüm numarası
|
||||||
|
/// tahmin etmeye gerek kalmaz (manuel modda numaralar `is_uefi`'ye göre
|
||||||
|
/// değil `assign_partition_numbers`'a göre belirlenir).
|
||||||
|
async fn luks_backing_device(name: &str) -> Result<String, String> {
|
||||||
|
if DEMO_MODE.load(Ordering::Relaxed) {
|
||||||
|
return Ok("/dev/demo-luks".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let out = tokio::process::Command::new("cryptsetup")
|
||||||
|
.args(["status", name])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("cryptsetup status {} çalıştırılamadı: {}", name, e))?;
|
||||||
|
|
||||||
|
// `cryptsetup status` stdout'a:
|
||||||
|
// /dev/mapper/cryptroot is active and is in use.
|
||||||
|
// type: LUKS2
|
||||||
|
// device: /dev/sda3
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
|
let device = stdout
|
||||||
|
.lines()
|
||||||
|
.find_map(|l| l.trim().strip_prefix("device:"))
|
||||||
|
.map(|d| d.trim().to_string())
|
||||||
|
.filter(|d| !d.is_empty());
|
||||||
|
|
||||||
|
device.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"{} mapper'ının altındaki aygıt çözümlenemedi. \
|
||||||
|
LUKS başlığı açılmamış olabilir; fstab/crypttab kontrol edin.",
|
||||||
|
name
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// initramfs yeniler (GRUB öncesi zorunlu).
|
/// initramfs yeniler (GRUB öncesi zorunlu).
|
||||||
/// PisiLinux `dracut` kullanıyorsa `dracut -f --regenerate-all`,
|
/// PisiLinux `dracut` kullanıyorsa `dracut -f --regenerate-all`,
|
||||||
/// mkinitcpio kullanıyorsa `mkinitcpio -P`.
|
/// mkinitcpio kullanıyorsa `mkinitcpio -P`.
|
||||||
@@ -180,11 +226,35 @@ impl Job for InstallGrubJob {
|
|||||||
// kernel cmdline
|
// kernel cmdline
|
||||||
let mut cmdline = self.kernel_options.clone();
|
let mut cmdline = self.kernel_options.clone();
|
||||||
if self.encrypt_root {
|
if self.encrypt_root {
|
||||||
// Kök bölüm UUID'sini blkid'den al (LUKS başlık UUID'si)
|
// LUKS başlık UUID'si GEREKLİ. Önceden bölüm numarası tahmin
|
||||||
let (root_num, _swap_num): (u32, u32) = if self.is_uefi { (3, 2) } else { (2, 1) };
|
// ediliyordu (UEFI'de 3, BIOS'ta 2) — bu manuel modda YANLIŞTIR
|
||||||
let root_part = part_path(&self.disk, root_num);
|
// (numaralar assign_partition_numbers'tan gelir) ve üstelik
|
||||||
let root_uuid = blkid_uuid(&root_part).await.unwrap_or_default();
|
// "cryptdevice=UUID=x:cryptroot" geçerli bir söz dizimi değildir.
|
||||||
let crypt_arg = format!("cryptdevice=UUID={}:cryptroot root=/dev/mapper/cryptroot", root_uuid);
|
//
|
||||||
|
// Bunun yerine çalışma zamanında gerçeği soruyoruz:
|
||||||
|
// `cryptsetup status` LUKS başlığının bulunduğu aygıtı verir,
|
||||||
|
// blkid ise o aygıtın LUKS UUID'sini (crypto_LUKS).
|
||||||
|
// /dev/mapper/cryptroot'un UUID'si değil — mapper'ın arkasındaki
|
||||||
|
// bölümünki kullanılır.
|
||||||
|
let luks_part = luks_backing_device(CRYPTROOT_NAME).await?;
|
||||||
|
let luks_uuid = blkid_uuid(&luks_part).await
|
||||||
|
.map_err(|e| format!("{} LUKS UUID okunamadı: {}", luks_part, e))?;
|
||||||
|
if luks_uuid.trim().is_empty() {
|
||||||
|
return Err(format!("{} için LUKS UUID boş geldi", luks_part));
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.log(format!("LUKS: {} → {}", luks_part, luks_uuid));
|
||||||
|
|
||||||
|
// dracut sözdizimi: rd.luks.uuid=luks-<uuid>
|
||||||
|
// mkinitcpio sözdizimi: cryptdevice=UUID=<uuid>:<mapper>
|
||||||
|
// PisiLinux hangisini kullandığı belli olmadığı için ikisi de
|
||||||
|
// yazılır; iki araç da diğerinin anahtarını yok sayar.
|
||||||
|
let crypt_arg = format!(
|
||||||
|
"rd.luks.uuid=luks-{uuid} rd.luks.name={uuid}={name} \
|
||||||
|
root=/dev/mapper/{name} cryptdevice=UUID={uuid}:{name}",
|
||||||
|
uuid = luks_uuid,
|
||||||
|
name = CRYPTROOT_NAME,
|
||||||
|
);
|
||||||
if cmdline.is_empty() {
|
if cmdline.is_empty() {
|
||||||
cmdline = crypt_arg;
|
cmdline = crypt_arg;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
//! Kurulum bir hata ile durduğunda veya bittiğinde çalışan temizlik işleri.
|
||||||
|
//!
|
||||||
|
//! Bu modülün varlık nedeni: `JobQueue::run_all` eskiden ilk hatada
|
||||||
|
//! `return` ediyordu. O an hedef bölümler bağlıydı, LUKS mapper'ları
|
||||||
|
//! açıktı ve swap aktifti — kullanıcı yeniden başlatmaya çalışırsa
|
||||||
|
//! sistem onu reddediyor, elle de düzeltmesi dakikalar sürüyordu.
|
||||||
|
//!
|
||||||
|
//! Temizlik işleri `JobQueue::with_cleanup(...)` ile verilir ve başarı
|
||||||
|
//! durumunda da, hata durumunda da çalışır.
|
||||||
|
|
||||||
|
use super::execution::umount_clean;
|
||||||
|
use super::{mounted_paths_under, Job, UiSender, DEMO_MODE};
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
/// Kurulum bitince veya hata ile durunca hedef sistemin bağlamalarını
|
||||||
|
/// tamamen çözer: tüm mount'lar, LUKS mapper'ları, LVM birimleri ve swap.
|
||||||
|
///
|
||||||
|
/// Sıralama önemlidir — en derinden yüzeye:
|
||||||
|
/// 1. `mount` altındaki tüm dosya sistemleri
|
||||||
|
/// 2. LVM mantıksal birimleri (`lvchange -an`)
|
||||||
|
/// 3. LUKS mapper'ları (`cryptsetup close`)
|
||||||
|
/// 4. Hedef diskin swap bölümü (`swapoff`)
|
||||||
|
///
|
||||||
|
/// Swap en sonda: swap'a yazmış bir süreç varsa erken swapoff denemesi
|
||||||
|
/// "device is busy" verir.
|
||||||
|
pub struct EmergencyCleanupJob {
|
||||||
|
pub mount: String,
|
||||||
|
/// Hedef diskin bölüm yolları (swapoff için). Boşsa otomatik bulunur.
|
||||||
|
pub partitions: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Job for EmergencyCleanupJob {
|
||||||
|
fn name(&self) -> &str { "job_cleanup_emergency" }
|
||||||
|
|
||||||
|
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||||
|
if DEMO_MODE.load(Ordering::Relaxed) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut errors: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
// ── 1. Dosya sistemleri ────────────────────────────────
|
||||||
|
let targets = mounted_paths_under(&self.mount);
|
||||||
|
if targets.is_empty() {
|
||||||
|
ui.log("Bağlı dosya sistemi yok");
|
||||||
|
}
|
||||||
|
for target in &targets {
|
||||||
|
if !umount_clean(target, ui).await {
|
||||||
|
errors.push(format!("{} bağlı kaldı", target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. LVM mantıksal birimleri ────────────────────────
|
||||||
|
// Etkin VG'lerin tüm LV'leri kapatılır. `vgs --noheadings` boş
|
||||||
|
// dönerse sistemde LVM yok demektir, hata değil.
|
||||||
|
if let Err(e) = deactivate_lvm(ui).await {
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3. LUKS mapper'ları ───────────────────────────────
|
||||||
|
// `lsblk` TYPE=crypt listesi. Hedef diskin LUKS bölümü hangisi
|
||||||
|
// ise onu kapatır; canlı ortamın kendi LUKS'u varsa dokunmaz.
|
||||||
|
match close_crypt_mappers(&self.partitions).await {
|
||||||
|
Ok(closed) => {
|
||||||
|
for name in closed {
|
||||||
|
ui.log(format!("LUKS kapatıldı: {}", name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => errors.push(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4. Swap ───────────────────────────────────────────
|
||||||
|
for part in &self.partitions {
|
||||||
|
if let Err(e) = swapoff_part(part, ui).await {
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(errors.join("\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Etkin tüm LVM VG'lerini deaktive eder.
|
||||||
|
async fn deactivate_lvm(ui: &UiSender) -> Result<(), String> {
|
||||||
|
let out = tokio::process::Command::new("vgs")
|
||||||
|
.args(["--noheadings", "-o", "vg_name"])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("vgs çalıştırılamadı: {}", e))?;
|
||||||
|
|
||||||
|
let vgs: Vec<String> = String::from_utf8_lossy(&out.stdout)
|
||||||
|
.lines()
|
||||||
|
.map(|l| l.trim().to_string())
|
||||||
|
.filter(|l| !l.is_empty())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if vgs.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut failed = Vec::new();
|
||||||
|
for vg in vgs {
|
||||||
|
ui.log(format!("LVM deaktive ediliyor: {}", vg));
|
||||||
|
match super::run_cmd("vgchange", &["-an", &vg]).await {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) => failed.push(format!("{} ({})", vg, e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if failed.is_empty() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("LVM deaktive edilemedi: {}", failed.join(", ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Açık LUKS mapper'larını kapatır.
|
||||||
|
///
|
||||||
|
/// `partitions` verilmişse yalnızca o bölümlerin mapper'ları kapatılır
|
||||||
|
/// (hedef diske ait olanlar). `cryptsetup luksUUID <dev>` ile eşleştirme
|
||||||
|
/// yapılır. Liste boşsa hiçbir şeye dokunulmaz — canlı ortamın kendi
|
||||||
|
/// şifreli bölümlerini kapatmak kullanıcının verisini riske atar.
|
||||||
|
async fn close_crypt_mappers(partitions: &[String]) -> Result<Vec<String>, String> {
|
||||||
|
if partitions.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mappers = super::open_crypt_mappers();
|
||||||
|
if mappers.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut closed = Vec::new();
|
||||||
|
for name in mappers {
|
||||||
|
// Bu mapper hangi bölümün arkasında?
|
||||||
|
let backing = backing_device(&name).await;
|
||||||
|
let belongs = backing
|
||||||
|
.as_deref()
|
||||||
|
.map(|d| partitions.iter().any(|p| p == d))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !belongs {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match super::run_cmd("cryptsetup", &["close", &name]).await {
|
||||||
|
Ok(()) => closed.push(name),
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!(
|
||||||
|
"cryptsetup close {} başarısız: {}. \
|
||||||
|
Hedef bölüm hâlâ kullanımda olabilir.",
|
||||||
|
name, e
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(closed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bir LUKS mapper'ının arkasındaki gerçek aygıtı döndürür.
|
||||||
|
async fn backing_device(mapper: &str) -> Option<String> {
|
||||||
|
let out = tokio::process::Command::new("cryptsetup")
|
||||||
|
.args(["status", mapper])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
|
String::from_utf8_lossy(&out.stdout)
|
||||||
|
.lines()
|
||||||
|
.find_map(|l| l.trim().strip_prefix("device:"))
|
||||||
|
.map(|d| d.trim().to_string())
|
||||||
|
.filter(|d| !d.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verilen bölüm swap ise `swapoff` çalıştırır. Swap değilse sessizce geçer.
|
||||||
|
async fn swapoff_part(part: &str, ui: &UiSender) -> Result<(), String> {
|
||||||
|
let out = tokio::process::Command::new("swapon")
|
||||||
|
.args(["--show", "--noheadings", "-o", "NAME"])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("swapon sorgusu başarısız: {}", e))?;
|
||||||
|
|
||||||
|
let active: Vec<String> = String::from_utf8_lossy(&out.stdout)
|
||||||
|
.lines()
|
||||||
|
.map(|l| l.trim().to_string())
|
||||||
|
.filter(|l| !l.is_empty())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !active.iter().any(|s| s == part) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.log(format!("swapoff {}", part));
|
||||||
|
super::run_cmd("swapoff", &[part]).await
|
||||||
|
.map_err(|e| format!("swapoff {} başarısız: {} (bu bölümü kullanan süreçleri kapatın)", part, e))
|
||||||
|
}
|
||||||
+18
-22
@@ -326,33 +326,29 @@ pub struct UnmountBindJob {
|
|||||||
impl Job for UnmountBindJob {
|
impl Job for UnmountBindJob {
|
||||||
fn name(&self) -> &str { "job_umount_bind" }
|
fn name(&self) -> &str { "job_umount_bind" }
|
||||||
|
|
||||||
|
/// `mount` altındaki TÜM bağlı dosya sistemlerini en derinden yüzeye
|
||||||
|
/// doğru çözer.
|
||||||
|
///
|
||||||
|
/// Önceden elle yazılmış sabit bir liste vardı (`proc`, `sys`, `dev`,
|
||||||
|
/// `boot/efi`, ...). Bu liste yalnızca otomatik modda doğruydu:
|
||||||
|
/// manuel bölümlemede `/home`, `/var`, `/boot` gibi ek bölümler
|
||||||
|
/// kurulum bitince bağlı kalıyordu. Artık düzen bilinmiyor —
|
||||||
|
/// `/proc/self/mountinfo` okunup `mount/` altındaki her şey bulunuyor.
|
||||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||||
let mt = &self.mount;
|
let mt = &self.mount;
|
||||||
// Ters sırada umount — en derin path önce
|
|
||||||
let subdirs = [
|
|
||||||
"sys/firmware/efi/efivars",
|
|
||||||
"dev/pts",
|
|
||||||
"dev",
|
|
||||||
"run",
|
|
||||||
"sys",
|
|
||||||
"proc",
|
|
||||||
"boot/efi",
|
|
||||||
];
|
|
||||||
let mut errors: Vec<String> = Vec::new();
|
let mut errors: Vec<String> = Vec::new();
|
||||||
|
|
||||||
// Bind-mount'lar + ESP (/boot/efi)
|
let mut targets = super::mounted_paths_under(mt);
|
||||||
for subdir in &subdirs {
|
|
||||||
let target = format!("{}/{}", mt, subdir);
|
if targets.is_empty() {
|
||||||
if !umount_clean(&target, ui).await {
|
ui.log("Çözülecek bağlama noktası yok (zaten temiz)");
|
||||||
let msg = format!("{} temiz unmount edilemedi", target);
|
targets.push(mt.clone());
|
||||||
errors.push(msg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kök bölüm: chroot mount'unun kendisi
|
for target in &targets {
|
||||||
if !umount_clean(mt, ui).await {
|
if !umount_clean(target, ui).await {
|
||||||
let msg = format!("{} (kök) temiz unmount edilemedi", mt);
|
errors.push(format!("{} temiz unmount edilemedi", target));
|
||||||
errors.push(msg);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !errors.is_empty() {
|
if !errors.is_empty() {
|
||||||
@@ -368,7 +364,7 @@ impl Job for UnmountBindJob {
|
|||||||
/// Tek bir mount noktasını önce temiz `umount` ile, başarısızsa `umount -lf`
|
/// Tek bir mount noktasını önce temiz `umount` ile, başarısızsa `umount -lf`
|
||||||
/// (lazy) ile çözer. "not mounted" / "no such file" gibi beklenen durumlar
|
/// (lazy) ile çözer. "not mounted" / "no such file" gibi beklenen durumlar
|
||||||
/// kritik sayılmaz; yalnızca komut çalıştırılamaması gerçek hatadır.
|
/// kritik sayılmaz; yalnızca komut çalıştırılamaması gerçek hatadır.
|
||||||
async fn umount_clean(target: &str, ui: &UiSender) -> bool {
|
pub(crate) async fn umount_clean(target: &str, ui: &UiSender) -> bool {
|
||||||
ui.log(format!("umount {}", target));
|
ui.log(format!("umount {}", target));
|
||||||
match tokio::process::Command::new("umount")
|
match tokio::process::Command::new("umount")
|
||||||
.arg(target).status().await
|
.arg(target).status().await
|
||||||
|
|||||||
+69
-46
@@ -1,8 +1,9 @@
|
|||||||
use super::{run_chroot, Job, UiSender};
|
use super::{run_chroot, Job, UiSender};
|
||||||
|
|
||||||
/// COMAR servislerini ve tetikleyicilerini yapılandırır.
|
/// `pisi configure-pending` (kısaltması `pisi cp`): kurulan paketlerin
|
||||||
|
/// post-install betiklerini çalıştırır. Bu adım olmadan kurulu sistemde
|
||||||
|
/// servisler/tetikleyiciler kurulmamış olur ve sistem düzgün açılmaz.
|
||||||
pub struct RunComarJob {
|
pub struct RunComarJob {
|
||||||
#[allow(dead_code)]
|
|
||||||
pub mount: String,
|
pub mount: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,27 +12,32 @@ impl Job for RunComarJob {
|
|||||||
fn name(&self) -> &str { "job_comar" }
|
fn name(&self) -> &str { "job_comar" }
|
||||||
|
|
||||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||||
// pisi configure-pending: paket post-install scriptlerini çalıştırır.
|
ui.log("pisi paketleri yapılandırılıyor (configure-pending)…");
|
||||||
// Yapılandırılacak paket yoksa çıkış kodu sıfır dışı olabilir — fatal değil.
|
match run_chroot(&self.mount, &["pisi", "cp"]).await {
|
||||||
// pisi cp işlemi kurulum sonrası atlanıyor (kullanıcı isteği).
|
Ok(_) => {
|
||||||
let _ = ui;
|
ui.log("✓ pisi paketleri yapılandırıldı");
|
||||||
// ui.log("pisi paketleri yapılandırılıyor (COMAR)…");
|
Ok(())
|
||||||
// match run_chroot(&self.mount, &["pisi", "cp"]).await {
|
}
|
||||||
// Ok(_) => {
|
Err(e) => {
|
||||||
// ui.log("✓ pisi paketleri yapılandırıldı");
|
// "Yapılandırılacak paket yok" bir hata değil; yeni kurulumda
|
||||||
// }
|
// (ör. yalnızca temel sistem) normal olarak oluşabilir.
|
||||||
// Err(e) => {
|
let benign = ["no packages to configure", "nothing to do",
|
||||||
// // "no packages to configure" gibi durumlar hata değil
|
"yapılandırılacak paket", "nothing to configure"];
|
||||||
// if e.contains("no packages") || e.contains("Nothing to do") {
|
if benign.iter().any(|m| e.to_lowercase().contains(m)) {
|
||||||
// ui.log(" pisi: yapılandırılacak paket yok, atlanıyor");
|
ui.log(" pisi: yapılandırılacak paket yok, atlanıyor");
|
||||||
// } else {
|
Ok(())
|
||||||
// // Gerçek hata — uyar ama kurulumu durdurma
|
} else {
|
||||||
// ui.log(format!("pisi paketleri yapılandırma uyarısı: {}", e));
|
// Post-install betikleri çalışmadı: bu, kurulumun
|
||||||
// ui.log("Bu uyarı kritik değilse kurulum devam edecek.");
|
// sessizce bozuk bırakılması anlamına gelir. Kurulumu
|
||||||
// }
|
// durdurup hatayı kullanıcıya göster.
|
||||||
// }
|
Err(format!(
|
||||||
// }
|
"pisi configure-pending başarısız: {}\n\
|
||||||
Ok(())
|
Paketlerin kurulum betikleri çalışmadı; sistem \
|
||||||
|
düzgün başlamayabilir.", e
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,15 +52,19 @@ impl Job for RunLdconfigJob {
|
|||||||
|
|
||||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||||
ui.log("ldconfig çalıştırılıyor…");
|
ui.log("ldconfig çalıştırılıyor…");
|
||||||
match run_chroot(&self.mount, &["ldconfig"]).await {
|
run_chroot(&self.mount, &["ldconfig"]).await
|
||||||
Ok(_) => ui.log("✓ ldconfig tamamlandı"),
|
.map_err(|e| format!("ldconfig başarısız: {}\n\
|
||||||
Err(e) => ui.log(format!("⚠ ldconfig uyarısı: {}", e)),
|
Paylaşımlı kütüphane önbelleği oluşturulamadı.", e))?;
|
||||||
}
|
ui.log("✓ ldconfig tamamlandı");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// update-environment: PisiLinux init/betiklerini yeniden yapılandırır.
|
/// update-environment: PisiLinux init/betiklerini yeniden yapılandırır.
|
||||||
|
///
|
||||||
|
/// Bu adım init sistemi için kritiktir (udev kuralları, servisler,
|
||||||
|
/// /etc/localtime senkronizasyonu). Sessizce yutulursa kurulum "başarılı"
|
||||||
|
/// görünür ama sistem açılmaz — bu yüzden hata fatal.
|
||||||
pub struct UpdateEnvironmentJob {
|
pub struct UpdateEnvironmentJob {
|
||||||
pub mount: String,
|
pub mount: String,
|
||||||
}
|
}
|
||||||
@@ -65,10 +75,11 @@ impl Job for UpdateEnvironmentJob {
|
|||||||
|
|
||||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||||
ui.log("update-environment çalıştırılıyor…");
|
ui.log("update-environment çalıştırılıyor…");
|
||||||
match run_chroot(&self.mount, &["update-environment"]).await {
|
run_chroot(&self.mount, &["update-environment"]).await
|
||||||
Ok(_) => ui.log("✓ update-environment tamamlandı"),
|
.map_err(|e| format!("update-environment başarısız: {}\n\
|
||||||
Err(e) => ui.log(format!("⚠ update-environment uyarısı: {}", e)),
|
Init betikleri yapılandırılmadı; kurulu sistem \
|
||||||
}
|
düzgün başlamayabilir.", e))?;
|
||||||
|
ui.log("✓ update-environment tamamlandı");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,29 +97,41 @@ impl Job for CleanupLiveJob {
|
|||||||
let mt = &self.mount;
|
let mt = &self.mount;
|
||||||
ui.log("Live ortam izleri temizleniyor…");
|
ui.log("Live ortam izleri temizleniyor…");
|
||||||
|
|
||||||
// run/livemedia
|
// Canlı ortamdan kalan dizinler: yoksa hata değil, `let _ =` uygun.
|
||||||
let _ = tokio::fs::remove_dir_all(format!("{}/run/livemedia", mt)).await;
|
let _ = tokio::fs::remove_dir_all(format!("{}/run/livemedia", mt)).await;
|
||||||
// var/cache/pisi/packages
|
|
||||||
let _ = tokio::fs::remove_dir_all(format!("{}/var/cache/pisi/packages", mt)).await;
|
let _ = tokio::fs::remove_dir_all(format!("{}/var/cache/pisi/packages", mt)).await;
|
||||||
|
let _ = tokio::fs::remove_dir_all(format!("{}/bootmnt", mt)).await;
|
||||||
// YALI desktop dosyaları
|
// YALI desktop dosyaları
|
||||||
//let _ = tokio::fs::remove_file(format!("{}/usr/share/applications/yali.desktop", mt)).await;
|
//let _ = tokio::fs::remove_file(format!("{}/usr/share/applications/yali.desktop", mt)).await;
|
||||||
//let _ = tokio::fs::remove_file(format!("{}/usr/share/applications/yali-bin.desktop", mt)).await;
|
//let _ = tokio::fs::remove_file(format!("{}/usr/share/applications/yali-bin.desktop", mt)).await;
|
||||||
let _ = tokio::fs::remove_dir_all(format!("{}/bootmnt", mt)).await;
|
|
||||||
//pisi kullanıcısını sil (ev diziniyle birlikte), YALI paketlerini kaldır
|
|
||||||
let _ = run_chroot(&self.mount, &["userdel", "-r", "pisi"]).await;
|
let _ = run_chroot(&self.mount, &["userdel", "-r", "pisi"]).await;
|
||||||
let _ = run_chroot(&self.mount, &["pisi", "rm", "yali-rs"]).await;
|
let _ = run_chroot(&self.mount, &["pisi", "rm", "yali-rs"]).await;
|
||||||
// live paket deposunu kaldır
|
|
||||||
let _ = run_chroot(&self.mount, &["pisi", "rr", "live"]).await;
|
let _ = run_chroot(&self.mount, &["pisi", "rr", "live"]).await;
|
||||||
// depolarları yeniden ekle (stable2 + contrib)
|
|
||||||
ui.log("Depolar Ekleniyor…");
|
// ── Aşağıdakiler FATAL ───────────────────────────────────
|
||||||
let _ = run_chroot(&self.mount, &["pisi", "ar", "stable2", "https://stable2.pisilinux.org/pisi-index.xml.xz"]).await;
|
// Depo tanımları yapılmazsa kurulu sistemde `pisi install`
|
||||||
let _ = run_chroot(&self.mount, &["pisi", "ar", "contrib", "https://contrib.pisilinux.org/pisi-index.xml.xz"]).await;
|
// çalışmaz, yani kullanıcı paket kuramaz. Sessizce yutulursa
|
||||||
// depoları aktif et
|
// kurulum "başarılı" görünür ama sistem kullanılamaz.
|
||||||
ui.log("Depolar aktif ediliyor...");
|
ui.log("Depolar ekleniyor…");
|
||||||
let _ = run_chroot(&self.mount, &["pisi", "er", "contrib", "stable2"]).await;
|
for (name, url) in [
|
||||||
// paket listelerini güncelle
|
("stable2", "https://stable2.pisilinux.org/pisi-index.xml.xz"),
|
||||||
ui.log("Depolar Güncelleniyor...");
|
("contrib", "https://contrib.pisilinux.org/pisi-index.xml.xz"),
|
||||||
let _ = run_chroot(&self.mount, &["pisi", "ur"]).await;
|
] {
|
||||||
|
run_chroot(&self.mount, &["pisi", "ar", name, url]).await
|
||||||
|
.map_err(|e| format!("'{}' deposu eklenemedi: {}\n\
|
||||||
|
Kurulu sistemde paket kurulamaz.", name, e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depoları etkinleştir
|
||||||
|
ui.log("Depolar etkinleştiriliyor…");
|
||||||
|
run_chroot(&self.mount, &["pisi", "er", "contrib", "stable2"]).await
|
||||||
|
.map_err(|e| format!("Depolar etkinleştirilemedi: {}", e))?;
|
||||||
|
|
||||||
|
// Paket listelerini güncelle (ağ gerektirir, birkaç dakika sürebilir)
|
||||||
|
ui.log("Depolar güncelleniyor (ağ gerekir, biraz sürebilir)…");
|
||||||
|
run_chroot(&self.mount, &["pisi", "ur"]).await
|
||||||
|
.map_err(|e| format!("Paket listeleri güncellenemedi: {}\n\
|
||||||
|
Depolar tanımlı ama indeksleri eksik olabilir.", e))?;
|
||||||
|
|
||||||
ui.log("✓ Live temizlik tamamlandı");
|
ui.log("✓ Live temizlik tamamlandı");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+182
-13
@@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
mod autologin;
|
mod autologin;
|
||||||
mod bootloader;
|
mod bootloader;
|
||||||
|
mod cleanup;
|
||||||
mod execution;
|
mod execution;
|
||||||
mod finish;
|
mod finish;
|
||||||
mod keyboard;
|
mod keyboard;
|
||||||
@@ -40,6 +41,7 @@ mod users;
|
|||||||
|
|
||||||
pub use autologin::ConfigureAutologinJob;
|
pub use autologin::ConfigureAutologinJob;
|
||||||
pub use bootloader::{GenerateInitramfsJob, InstallGrubJob};
|
pub use bootloader::{GenerateInitramfsJob, InstallGrubJob};
|
||||||
|
pub use cleanup::EmergencyCleanupJob;
|
||||||
pub use execution::{CopyFilesJob, MountBindJob, UnmountBindJob};
|
pub use execution::{CopyFilesJob, MountBindJob, UnmountBindJob};
|
||||||
pub use finish::{CleanupLiveJob, RunComarJob, RunLdconfigJob, UpdateEnvironmentJob};
|
pub use finish::{CleanupLiveJob, RunComarJob, RunLdconfigJob, UpdateEnvironmentJob};
|
||||||
pub use keyboard::ConfigureKeyboardJob;
|
pub use keyboard::ConfigureKeyboardJob;
|
||||||
@@ -165,15 +167,31 @@ pub trait Job: Send + Sync {
|
|||||||
|
|
||||||
pub struct JobQueue {
|
pub struct JobQueue {
|
||||||
jobs: Vec<Box<dyn Job>>,
|
jobs: Vec<Box<dyn Job>>,
|
||||||
|
/// Başarı da başarısızlık da HER ZAMAN çalıştırılır. Bir iş hata
|
||||||
|
/// verdiğinde bu listeye geçilmeden `return` edilirse hedef bölümler
|
||||||
|
/// bağlı kalır, LUKS mapper'ları açık, swap aktif ve kullanıcı
|
||||||
|
/// kullanılamaz bir canlı sistemde kalır.
|
||||||
|
cleanup: Vec<Box<dyn Job>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JobQueue {
|
impl JobQueue {
|
||||||
pub fn new(jobs: Vec<Box<dyn Job>>) -> Self {
|
pub fn new(jobs: Vec<Box<dyn Job>>) -> Self {
|
||||||
Self { jobs }
|
Self { jobs, cleanup: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `cleanup` işleri kuyruk BİTSE BİLE hata durumunda da çalıştırılır.
|
||||||
|
pub fn with_cleanup(mut self, cleanup: Vec<Box<dyn Job>>) -> Self {
|
||||||
|
self.cleanup = cleanup;
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tüm işleri sırayla çalıştırır.
|
/// Tüm işleri sırayla çalıştırır.
|
||||||
/// İlerleme ve loglar `ui` üzerinden UI'ye iletilir.
|
/// İlerleme ve loglar `ui` üzerinden UI'ye iletilir.
|
||||||
|
///
|
||||||
|
/// Hata durumunda: hatayı loglar, `cleanup` işlerini çalıştırır,
|
||||||
|
/// ardından `InstallMessage::Error` gönderir. Temizlik de hata
|
||||||
|
/// verirse bu hatalar orijinal hataya eklenerek raporlanır — böylece
|
||||||
|
/// kullanıcı hem asıl hatayı hem de "ne yapılması gerektiğini" görür.
|
||||||
pub async fn run_all(&mut self, ui: UiSender) {
|
pub async fn run_all(&mut self, ui: UiSender) {
|
||||||
let total = self.jobs.len();
|
let total = self.jobs.len();
|
||||||
|
|
||||||
@@ -181,24 +199,52 @@ impl JobQueue {
|
|||||||
let localized_name = t!(job.name());
|
let localized_name = t!(job.name());
|
||||||
ui.log(format!("▶ {}", localized_name));
|
ui.log(format!("▶ {}", localized_name));
|
||||||
|
|
||||||
match job.run(&ui).await {
|
if let Err(e) = job.run(&ui).await {
|
||||||
Ok(()) => {
|
let msg = format!("✗ {}: {}", localized_name, e);
|
||||||
ui.log(format!("✓ {}", localized_name));
|
ui.log(msg.clone());
|
||||||
}
|
let report = self.run_cleanup(&ui, &e).await;
|
||||||
Err(e) => {
|
ui.send(InstallMessage::Error(report));
|
||||||
let msg = format!("✗ {}: {}", localized_name, e);
|
return;
|
||||||
ui.log(msg.clone());
|
|
||||||
ui.send(InstallMessage::Error(msg));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ui.log(format!("✓ {}", localized_name));
|
||||||
let progress = (i + 1) as f32 / total as f32;
|
let progress = (i + 1) as f32 / total as f32;
|
||||||
ui.send(InstallMessage::Progress(progress));
|
ui.send(InstallMessage::Progress(progress));
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.send(InstallMessage::Done);
|
ui.send(InstallMessage::Done);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Temizlik işlerini çalıştırır; kullanıcıya gösterilecek rapor döndürür.
|
||||||
|
async fn run_cleanup(&self, ui: &UiSender, original_err: &str) -> String {
|
||||||
|
if self.cleanup.is_empty() {
|
||||||
|
return original_err.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.log("Kurulum durdu — bağlamalar temizleniyor…");
|
||||||
|
let mut leftovers: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
for job in &self.cleanup {
|
||||||
|
match job.run(ui).await {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) => leftovers.push(format!("{}: {}", t!(job.name()), e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if leftovers.is_empty() {
|
||||||
|
ui.log("✓ Bağlamalar temizlendi");
|
||||||
|
original_err.to_string()
|
||||||
|
} else {
|
||||||
|
ui.log(format!("⚠ {} temizlik sorunu", leftovers.len()));
|
||||||
|
let msg = format!(
|
||||||
|
"{}\n\n--- KURULUM DURDUKTAN SONRA ---\n\
|
||||||
|
Canlı sistemde elle temizlemeniz gerekenler var:\n{}",
|
||||||
|
original_err,
|
||||||
|
leftovers.iter().map(|l| format!(" • {}", l)).collect::<Vec<_>>().join("\n")
|
||||||
|
);
|
||||||
|
msg
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
@@ -281,6 +327,90 @@ fn is_mount_point(path: &std::path::Path) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `/proc/self/mountinfo` alanındaki kaçışlı diziyi çözer.
|
||||||
|
///
|
||||||
|
/// mountinfo, yol içindeki özel karakterleri `\0NNN` **oktal** olarak yazar
|
||||||
|
/// (boşluk `\040`, tab `\011`, satır sonu `\012`, backslash `\134`).
|
||||||
|
/// Çözülmezse "My Disk" gibi bir mountpoint hiçbir koşulla eşleştirilemez.
|
||||||
|
fn unescape_mount_field(s: &str) -> String {
|
||||||
|
let bytes = s.as_bytes();
|
||||||
|
let mut out = Vec::with_capacity(bytes.len());
|
||||||
|
let mut i = 0;
|
||||||
|
while i < bytes.len() {
|
||||||
|
if bytes[i] == b'\\' && i + 3 < bytes.len() + 1 {
|
||||||
|
// `\0NNN` veya `\NNN` biçiminde 3 haneli oktal
|
||||||
|
let (start, width) = if bytes.get(i + 1) == Some(&b'0') { (i + 1, 4) } else { (i + 1, 3) };
|
||||||
|
if start + width <= bytes.len() {
|
||||||
|
let digits = &s[start..start + width];
|
||||||
|
if digits.bytes().all(|b| (b'0'..=b'7').contains(&b)) {
|
||||||
|
if let Ok(v) = u32::from_str_radix(digits, 8) {
|
||||||
|
out.push(v as u8);
|
||||||
|
i = start + width;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push(bytes[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
String::from_utf8_lossy(&out).into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bir bağlama noktasının altındaki TÜM bağlı dosya sistemlerini döndürür,
|
||||||
|
/// en derinden yüzeye doğru sıralı.
|
||||||
|
///
|
||||||
|
/// Neden gerekiyor: `UnmountBindJob` elle yazılmış bir liste
|
||||||
|
/// (`proc`, `sys`, `dev`, `boot/efi`, ...) kullanıyordu. Manuel bölümlemede
|
||||||
|
/// `/home`, `/var`, `/boot` gibi ek bölümler o listede yoktu ve kurulum
|
||||||
|
/// sonrası bağlı kalıyordu. Bu fonksiyon düzeni bilmez — `mount` altındaki
|
||||||
|
/// her şeyi bulur, LUKS/LVM/fuse/loop ne olursa olsun.
|
||||||
|
fn mounted_paths_under(mount: &str) -> Vec<String> {
|
||||||
|
let prefix = format!("{}/", mount.trim_end_matches('/'));
|
||||||
|
let root = mount.trim_end_matches('/').to_string();
|
||||||
|
|
||||||
|
let mut paths: Vec<String> = Vec::new();
|
||||||
|
if let Ok(info) = std::fs::read_to_string("/proc/self/mountinfo") {
|
||||||
|
for line in info.lines() {
|
||||||
|
// mountinfo alanları: id parent major:minor root mountpoint
|
||||||
|
// options [optional...] - fstype source superopts
|
||||||
|
let Some(mp_field) = line.split_whitespace().nth(4) else { continue };
|
||||||
|
let mp = unescape_mount_field(mp_field);
|
||||||
|
if mp == root || mp.starts_with(&prefix) {
|
||||||
|
paths.push(mp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `/proc/self/mountinfo` kökte de bir satır yoksa kökü de dahil et
|
||||||
|
if !paths.iter().any(|p| p == &root) && is_mount_point(std::path::Path::new(&root)) {
|
||||||
|
paths.push(root.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// En derin yol önce: "/mnt/a/b" -> "/mnt/a" -> "/mnt".
|
||||||
|
// Aynı derinlikte ise uzun yol önce.
|
||||||
|
paths.sort_by(|a, b| b.len().cmp(&a.len()));
|
||||||
|
paths.dedup();
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
|
/// LUKS mapper adlarını döndürür (typeseri `crypt` olan aygıtlar).
|
||||||
|
fn open_crypt_mappers() -> Vec<String> {
|
||||||
|
let out = std::process::Command::new("lsblk")
|
||||||
|
.args(["-ndo", "NAME,TYPE"])
|
||||||
|
.output();
|
||||||
|
let Ok(out) = out else { return Vec::new() };
|
||||||
|
String::from_utf8_lossy(&out.stdout)
|
||||||
|
.lines()
|
||||||
|
.filter_map(|l| {
|
||||||
|
let mut it = l.split_whitespace();
|
||||||
|
let name = it.next()?;
|
||||||
|
let kind = it.next()?;
|
||||||
|
(kind == "crypt").then(|| name.to_string())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// `blkid` ile bir bölümün dosya sistemi türünü döndürür.
|
/// `blkid` ile bir bölümün dosya sistemi türünü döndürür.
|
||||||
async fn get_fs_type(device: &str) -> Result<String, String> {
|
async fn get_fs_type(device: &str) -> Result<String, String> {
|
||||||
let output = tokio::process::Command::new("blkid")
|
let output = tokio::process::Command::new("blkid")
|
||||||
@@ -511,6 +641,8 @@ pub fn build_full_job_queue(
|
|||||||
let mt = cfg.mount.clone();
|
let mt = cfg.mount.clone();
|
||||||
let plan2 = plan.clone();
|
let plan2 = plan.clone();
|
||||||
let plan3 = plan.clone();
|
let plan3 = plan.clone();
|
||||||
|
// Temizlik için bölüm yolları; `plan` birkaç satır sonra taşınıyor.
|
||||||
|
let cleanup_parts = auto_mode_partitions(&plan);
|
||||||
|
|
||||||
// Kaynak dizini: parametre boşsa otomatik tespit et
|
// Kaynak dizini: parametre boşsa otomatik tespit et
|
||||||
let src = if cfg.source.is_empty() { detect_source_dir() } else { cfg.source };
|
let src = if cfg.source.is_empty() { detect_source_dir() } else { cfg.source };
|
||||||
@@ -613,7 +745,30 @@ pub fn build_full_job_queue(
|
|||||||
// 20. Bind-mount temizle
|
// 20. Bind-mount temizle
|
||||||
jobs.push(Box::new(UnmountBindJob { mount: mt.clone() }));
|
jobs.push(Box::new(UnmountBindJob { mount: mt.clone() }));
|
||||||
|
|
||||||
JobQueue::new(jobs)
|
// 21. Son temizlik: LUKS mapper'larını kapat, swap'u indir.
|
||||||
|
// Normal akışta da çalışır — yeniden başlatmadan önce mapper'ın
|
||||||
|
// açık kalması gereksiz ve zararlıdır.
|
||||||
|
jobs.push(Box::new(EmergencyCleanupJob {
|
||||||
|
mount: mt.clone(),
|
||||||
|
partitions: cleanup_parts.clone(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
JobQueue::new(jobs).with_cleanup(vec![Box::new(EmergencyCleanupJob {
|
||||||
|
mount: mt.clone(),
|
||||||
|
partitions: cleanup_parts,
|
||||||
|
})])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Otomatik modda oluşturulan bölüm yolları (temizlik için).
|
||||||
|
/// Sıra `PartitionJob` ile aynı olmalıdır: EFI → swap → kök.
|
||||||
|
fn auto_mode_partitions(plan: &PartitionPlan) -> Vec<String> {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
if plan.efi_mb > 0 {
|
||||||
|
parts.push(part_path(&plan.disk, 1));
|
||||||
|
}
|
||||||
|
parts.push(part_path(&plan.disk, if plan.efi_mb > 0 { 2 } else { 1 })); // swap
|
||||||
|
parts.push(part_path(&plan.disk, if plan.efi_mb > 0 { 3 } else { 2 })); // kök
|
||||||
|
parts
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manuel bölümleme kullanıldığında derlenecek kurulum iş kuyruğu.
|
/// Manuel bölümleme kullanıldığında derlenecek kurulum iş kuyruğu.
|
||||||
@@ -686,5 +841,19 @@ pub fn build_custom_job_queue(
|
|||||||
jobs.push(Box::new(CleanupLiveJob { mount: mt.clone() }));
|
jobs.push(Box::new(CleanupLiveJob { mount: mt.clone() }));
|
||||||
jobs.push(Box::new(UnmountBindJob { mount: mt.clone() }));
|
jobs.push(Box::new(UnmountBindJob { mount: mt.clone() }));
|
||||||
|
|
||||||
JobQueue::new(jobs)
|
// Son temizlik (bkz. otomatik mod).
|
||||||
|
let cleanup_parts: Vec<String> = custom_partitions.iter()
|
||||||
|
.filter(|p| !p.delete)
|
||||||
|
.map(|p| p.device.clone())
|
||||||
|
.filter(|d| !d.is_empty())
|
||||||
|
.collect();
|
||||||
|
jobs.push(Box::new(EmergencyCleanupJob {
|
||||||
|
mount: mt.clone(),
|
||||||
|
partitions: cleanup_parts.clone(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
JobQueue::new(jobs).with_cleanup(vec![Box::new(EmergencyCleanupJob {
|
||||||
|
mount: mt.clone(),
|
||||||
|
partitions: cleanup_parts,
|
||||||
|
})])
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-8
@@ -196,6 +196,16 @@ impl Job for PartitionJob {
|
|||||||
let disk = &self.plan.disk;
|
let disk = &self.plan.disk;
|
||||||
let is_gpt = self.plan.table_type == PartitionTableType::Gpt;
|
let is_gpt = self.plan.table_type == PartitionTableType::Gpt;
|
||||||
|
|
||||||
|
// encrypt_root açık ama parola boşsa kök düz ext4 olarak formatlanır,
|
||||||
|
// MountPartitionsJob / GenerateFstabJob ise yalnızca `encrypt_root`
|
||||||
|
// bakıp /dev/mapper/cryptroot'a mount etmeye çalışır → kurulum
|
||||||
|
// yarıda ölür. Bu kombinasyon UI'dan da engellenmeli.
|
||||||
|
if self.plan.encrypt_root && self.plan.luks_password.is_empty() {
|
||||||
|
return Err("Kök şifreleme (LUKS) seçilmiş ancak parola boş. \
|
||||||
|
Lütfen parola girin veya şifrelemeyi kapatın."
|
||||||
|
.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
// 0. Diski hazırla: bağlamaları ayır, kernel tablosunu temizle
|
// 0. Diski hazırla: bağlamaları ayır, kernel tablosunu temizle
|
||||||
prepare_disk(disk, ui).await;
|
prepare_disk(disk, ui).await;
|
||||||
|
|
||||||
@@ -229,11 +239,16 @@ impl Job for PartitionJob {
|
|||||||
next_mb = swap_end;
|
next_mb = swap_end;
|
||||||
|
|
||||||
// 4. Kök bölümü (kalan alan)
|
// 4. Kök bölümü (kalan alan)
|
||||||
|
// DİKKAT: parted "MB" sonekini ONDALIK (10^6) okur, "MiB" ikili
|
||||||
|
// (2^20). Hesaplarımız ikili olduğu için daima "MiB" geçilir.
|
||||||
|
// "MB" ile 4609MB = 9001992.1875 sektör → hizasız başlangıç →
|
||||||
|
// "Becerebildiğimiz buna en yakın konum..." hatası. Tamsayı MiB
|
||||||
|
// 2048 sektörün katıdır, hizalama sorunu olmaz.
|
||||||
let root_num: u32 = if self.plan.efi_mb > 0 { 3 } else { 2 };
|
let root_num: u32 = if self.plan.efi_mb > 0 { 3 } else { 2 };
|
||||||
ui.log(format!("Kök bölümü oluşturuluyor: {}MiB – disk sonu", next_mb));
|
ui.log(format!("Kök bölümü oluşturuluyor: {}MiB – disk sonu", next_mb));
|
||||||
run_parted(&[
|
run_parted(&[
|
||||||
"-s", disk, "mkpart", "ext4",
|
"-s", disk, "mkpart", "ext4",
|
||||||
&format!("{}MB", next_mb), "100%",
|
&format!("{}MiB", next_mb), "100%",
|
||||||
]).await?;
|
]).await?;
|
||||||
|
|
||||||
// 4.5. Tüm mkpart bitti; kernel'e bildir ve aygıt düğümlerini bekle
|
// 4.5. Tüm mkpart bitti; kernel'e bildir ve aygıt düğümlerini bekle
|
||||||
@@ -258,8 +273,9 @@ impl Job for PartitionJob {
|
|||||||
// LUKS şifreleme (kök bölümü)
|
// LUKS şifreleme (kök bölümü)
|
||||||
// Şifre, shell injection'ı önlemek için doğrudan stdin pipe üzerinden
|
// Şifre, shell injection'ı önlemek için doğrudan stdin pipe üzerinden
|
||||||
// iletilir; `sh -c` + string interpolasyon kullanılmaz.
|
// iletilir; `sh -c` + string interpolasyon kullanılmaz.
|
||||||
let root_device = if self.plan.encrypt_root && !self.plan.luks_password.is_empty() {
|
// (Parolanın boş olma durumu yukarıda reddedildi.)
|
||||||
let luks_name = "cryptroot";
|
let root_device = if self.plan.encrypt_root {
|
||||||
|
let luks_name = super::bootloader::CRYPTROOT_NAME;
|
||||||
ui.log(format!("cryptsetup luksFormat {}…", root_part));
|
ui.log(format!("cryptsetup luksFormat {}…", root_part));
|
||||||
run_luks_format(&root_part, &self.plan.luks_password).await
|
run_luks_format(&root_part, &self.plan.luks_password).await
|
||||||
.map_err(|e| format!("LUKS format hatası: {}", e))?;
|
.map_err(|e| format!("LUKS format hatası: {}", e))?;
|
||||||
@@ -297,7 +313,7 @@ impl Job for MountPartitionsJob {
|
|||||||
let root_part = part_path(disk, root_num);
|
let root_part = part_path(disk, root_num);
|
||||||
|
|
||||||
let root_device = if self.plan.encrypt_root {
|
let root_device = if self.plan.encrypt_root {
|
||||||
"/dev/mapper/cryptroot".to_string()
|
format!("/dev/mapper/{}", super::bootloader::CRYPTROOT_NAME)
|
||||||
} else {
|
} else {
|
||||||
root_part.clone()
|
root_part.clone()
|
||||||
};
|
};
|
||||||
@@ -378,9 +394,10 @@ impl Job for GenerateFstabJob {
|
|||||||
|
|
||||||
// Kök bölümü
|
// Kök bölümü
|
||||||
if self.encrypt_root {
|
if self.encrypt_root {
|
||||||
fstab.push_str(
|
fstab.push_str(&format!(
|
||||||
"/dev/mapper/cryptroot / ext4 defaults,noatime 0 1\n"
|
"/dev/mapper/{} / ext4 defaults,noatime 0 1\n",
|
||||||
);
|
super::bootloader::CRYPTROOT_NAME
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
fstab.push_str(&format!(
|
fstab.push_str(&format!(
|
||||||
"UUID={} / ext4 defaults,noatime 0 1\n",
|
"UUID={} / ext4 defaults,noatime 0 1\n",
|
||||||
@@ -410,7 +427,8 @@ impl Job for GenerateFstabJob {
|
|||||||
if self.encrypt_root {
|
if self.encrypt_root {
|
||||||
let crypttab_path = format!("{}/etc/crypttab", self.mount);
|
let crypttab_path = format!("{}/etc/crypttab", self.mount);
|
||||||
let crypttab = format!(
|
let crypttab = format!(
|
||||||
"cryptroot UUID={} none luks\n",
|
"{} UUID={} none luks\n",
|
||||||
|
super::bootloader::CRYPTROOT_NAME,
|
||||||
self.root_uuid
|
self.root_uuid
|
||||||
);
|
);
|
||||||
ui.log(format!("Yazılıyor: {}", crypttab_path));
|
ui.log(format!("Yazılıyor: {}", crypttab_path));
|
||||||
|
|||||||
+65
-56
@@ -24,7 +24,7 @@ impl Job for CreateUserJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// useradd: kullanıcıyı temel gruplarla oluştur
|
// useradd: kullanıcıyı temel gruplarla oluştur
|
||||||
// PisiLinux'ta "storage" grubu yoksa useradd hata verir; -M flag ile grub yoksa atla
|
// PisiLinux'ta "storage" grubu yoksa useradd hata verir; -M flag ile grup yoksa atla
|
||||||
// Güvenli yöntem: önce her grubu tek tek ekle
|
// Güvenli yöntem: önce her grubu tek tek ekle
|
||||||
let groups = ["wheel", "users", "pnp", "disk", "audio", "video", "power", "dialout", "lp", "lpadmin", "cdrom", "floppy"];
|
let groups = ["wheel", "users", "pnp", "disk", "audio", "video", "power", "dialout", "lp", "lpadmin", "cdrom", "floppy"];
|
||||||
for grp in &groups {
|
for grp in &groups {
|
||||||
@@ -47,37 +47,22 @@ impl Job for CreateUserJob {
|
|||||||
&self.username,
|
&self.username,
|
||||||
]).await?;
|
]).await?;
|
||||||
|
|
||||||
// // chpasswd ile şifre ayarla (stdin üzerinden güvenli)
|
// Kullanıcı şifresi: `chpasswd` stdin üzerinden.
|
||||||
// ui.log(format!("Şifre ayarlanıyor: {}", self.username));
|
// `sh -c "echo 'user:pass' | ..."` KULLANILMAZ: parola düz metin
|
||||||
// let chpasswd_input = format!("{}:{}", self.username, self.password);
|
// olarak kabuğun argv'sine geçer ve /proc/<pid>/cmdline üzerinden
|
||||||
// set_password_via_chpasswd(&self.mount, &chpasswd_input).await?;
|
// okunabilir; ayrıca `'` veya `$(...)` içeren parola komutu bozar.
|
||||||
|
// Aynı desen jobs/partition.rs::run_luks_format'ta da kullanılıyor.
|
||||||
// // Root şifresi: belirlenmişse ayarla, yoksa kilitle
|
|
||||||
// if self.root_password.is_empty() {
|
|
||||||
// ui.log("Root hesabı kilitleniyor…");
|
|
||||||
// run_chroot(&self.mount, &["passwd", "-l", "root"]).await?;
|
|
||||||
// } else {
|
|
||||||
// ui.log("Root şifresi ayarlanıyor…");
|
|
||||||
// let root_input = format!("root:{}", self.root_password);
|
|
||||||
// set_password_via_chpasswd(&self.mount, &root_input).await?;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 2. chpasswd ile şifre ayarla (YENİ BASİTLEŞTİRİLMİŞ VE KESİN YÖNTEM)
|
|
||||||
ui.log(format!("Şifre ayarlanıyor: {}", self.username));
|
ui.log(format!("Şifre ayarlanıyor: {}", self.username));
|
||||||
// sh -c kullanarak echo ile chpasswd'e şifreyi güvenli bir şekilde üflüyoruz
|
set_password_via_chpasswd(&self.mount, &self.username, &self.password).await?;
|
||||||
let user_cmd = format!("echo '{}:{}' | chpasswd -c SHA512", self.username, self.password);
|
|
||||||
run_chroot(&self.mount, &["sh", "-c", &user_cmd]).await?;
|
|
||||||
|
|
||||||
// 3. Root şifresi: belirlenmişse ayarla, yoksa kilitle
|
// Root şifresi: belirlenmişse ayarla, yoksa kilitle
|
||||||
if self.root_password.is_empty() {
|
if self.root_password.is_empty() {
|
||||||
ui.log("Root hesabı kilitleniyor…");
|
ui.log("Root hesabı kilitleniyor…");
|
||||||
run_chroot(&self.mount, &["passwd", "-l", "root"]).await?;
|
run_chroot(&self.mount, &["passwd", "-l", "root"]).await?;
|
||||||
} else {
|
} else {
|
||||||
ui.log("Root şifresi ayarlanıyor…");
|
ui.log("Root şifresi ayarlanıyor…");
|
||||||
let root_cmd = format!("echo 'root:{}' | chpasswd -c SHA512", self.root_password);
|
set_password_via_chpasswd(&self.mount, "root", &self.root_password).await?;
|
||||||
run_chroot(&self.mount, &["sh", "-c", &root_cmd]).await?;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// sudoers: wheel grubu sudo yetkisi alsın
|
// sudoers: wheel grubu sudo yetkisi alsın
|
||||||
let sudoers_dir = format!("{}/etc/sudoers.d", self.mount);
|
let sudoers_dir = format!("{}/etc/sudoers.d", self.mount);
|
||||||
@@ -96,39 +81,63 @@ impl Job for CreateUserJob {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// /// `chpasswd` komutuna stdin üzerinden kullanıcı:şifre gönderir.
|
/// `chroot <mount> chpasswd -c SHA512` komutuna `kullanici:sifre` bilgisini
|
||||||
// async fn set_password_via_chpasswd(mount: &str, input: &str) -> Result<(), String> {
|
/// **stdin pipe'ı** üzerinden iletir.
|
||||||
// if DEMO_MODE.load(std::sync::atomic::Ordering::Relaxed) {
|
///
|
||||||
// tokio::time::sleep(std::time::Duration::from_millis(2500)).await;
|
/// Neden `sh -c` + `format!` değil:
|
||||||
// return Ok(());
|
/// 1. Parola kabuğun argv'sine yazılırsa `/proc/<pid>/cmdline` üzerinden
|
||||||
// }
|
/// canlı sistemdeki herhangi bir süreç tarafından okunabilir.
|
||||||
|
/// 2. Parola içindeki `'` veya `$(...)` karakterleri komutu bozar ve
|
||||||
|
/// shell injection'a yol açar.
|
||||||
|
/// 3. `format!` ile üretilen komut `user:pass` içinde `:` geçse hatalı
|
||||||
|
/// ayrıştırılır.
|
||||||
|
///
|
||||||
|
/// Bu desen `jobs/partition.rs::run_luks_format` ile birebir aynıdır.
|
||||||
|
async fn set_password_via_chpasswd(
|
||||||
|
mount: &str,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if super::DEMO_MODE.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(2500)).await;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
// use tokio::process::Command;
|
|
||||||
|
|
||||||
// let mut child = Command::new("chroot")
|
let mut child = tokio::process::Command::new("chroot")
|
||||||
// .args([mount, "chpasswd"])
|
.args([mount, "chpasswd", "-c", "SHA512"])
|
||||||
// .stdin(std::process::Stdio::piped())
|
.stdin(std::process::Stdio::piped())
|
||||||
// .stdout(std::process::Stdio::null())
|
.stdout(std::process::Stdio::null())
|
||||||
// .stderr(std::process::Stdio::piped())
|
.stderr(std::process::Stdio::piped())
|
||||||
// .spawn()
|
.spawn()
|
||||||
// .map_err(|e| format!("chpasswd başlatılamadı: {}", e))?;
|
.map_err(|e| format!("chpasswd başlatılamadı: {}", e))?;
|
||||||
|
|
||||||
// if let Some(mut stdin) = child.stdin.take() {
|
// Parolayı yalnızca çocuk sürecin stdin'ine yaz; argv'de hiç görünmez.
|
||||||
// let mut data = input.as_bytes().to_vec();
|
if let Some(mut stdin) = child.stdin.take() {
|
||||||
// if !data.ends_with(b"\n") {
|
// chpasswd "user:pass" satırı bekler ve satır sonu ile ayırır.
|
||||||
// data.push(b'\n');
|
let mut data = format!("{}:{}\n", username, password).into_bytes();
|
||||||
// }
|
if data.last() != Some(&b'\n') {
|
||||||
// stdin.write_all(&data).await
|
data.push(b'\n');
|
||||||
// .map_err(|e| format!("chpasswd stdin yazma hatası: {}", e))?;
|
}
|
||||||
// }
|
let result = stdin.write_all(&data).await;
|
||||||
|
// Yazdıktan sonra parolanın kopyasını bellekte bırakma.
|
||||||
|
data.iter_mut().for_each(|b| *b = 0);
|
||||||
|
drop(stdin);
|
||||||
|
result.map_err(|e| format!("chpasswd stdin yazma hatası: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
// let output = child.wait_with_output().await
|
let output = child.wait_with_output().await
|
||||||
// .map_err(|e| format!("chpasswd bekleme hatası: {}", e))?;
|
.map_err(|e| format!("chpasswd bekleme hatası: {}", e))?;
|
||||||
|
|
||||||
// if output.status.success() {
|
if output.status.success() {
|
||||||
// Ok(())
|
Ok(())
|
||||||
// } else {
|
} else {
|
||||||
// Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
|
let err = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
// }
|
if err.is_empty() {
|
||||||
// }
|
Err(format!("{} için şifre ayarlanamadı", username))
|
||||||
|
} else {
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+86
-10
@@ -126,11 +126,20 @@ impl RescueStep {
|
|||||||
let op: u32 = op_str.parse().unwrap_or(0);
|
let op: u32 = op_str.parse().unwrap_or(0);
|
||||||
|
|
||||||
// Tarih: "[2024-03-15 12:00:01]"
|
// Tarih: "[2024-03-15 12:00:01]"
|
||||||
let date = if let Some(start) = remainder.find('[') {
|
// `]` ve `[` bağımsız aranırsa, `]` önce gelen bir girdide
|
||||||
if let Some(end) = remainder.find(']') {
|
// `end < start` olur ve `remainder[start+1..end]` PANİK verir
|
||||||
remainder[start+1..end].to_string()
|
// ("slice index starts at N but ends at M"). Kapanış
|
||||||
} else { String::new() }
|
// parantez, açılıştan SONRA aranmalı.
|
||||||
} else { String::new() };
|
let date = match remainder.find('[') {
|
||||||
|
None => String::new(),
|
||||||
|
Some(open) => {
|
||||||
|
let inner = open + 1;
|
||||||
|
match remainder[inner..].find(']') {
|
||||||
|
Some(rel_close) => remainder[inner..inner + rel_close].to_string(),
|
||||||
|
None => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Tür + özet: ": install [vim, curl]"
|
// Tür + özet: ": install [vim, curl]"
|
||||||
let (op_type, summary) = if let Some(colon) = remainder.rfind("]: ") {
|
let (op_type, summary) = if let Some(colon) = remainder.rfind("]: ") {
|
||||||
@@ -372,11 +381,16 @@ impl RescueStep {
|
|||||||
ui.colored_label(type_color, &entry.op_type);
|
ui.colored_label(type_color, &entry.op_type);
|
||||||
|
|
||||||
// Özet — truncate
|
// Özet — truncate
|
||||||
let summary = if entry.summary.len() > 50 {
|
// `len()` bayt sayar, `&s[..50]` ise bayt
|
||||||
format!("{}…", &entry.summary[..50])
|
// sınırı ister. `pisi history` çıktısı paket
|
||||||
} else {
|
// adlarından gelir ve Türkçe karakter içerir
|
||||||
entry.summary.clone()
|
// ("İ", "ş", "ğ" 2 bayt): `[..50]` karakter
|
||||||
};
|
// sınırının ortasına denk gelirse PANİK.
|
||||||
|
// Karakter sayarak kırpmak gerekir.
|
||||||
|
let mut summary: String = entry.summary.chars().take(50).collect();
|
||||||
|
if entry.summary.chars().count() > 50 {
|
||||||
|
summary.push('…');
|
||||||
|
}
|
||||||
ui.label(egui::RichText::new(summary).size(11.0).color(theme::c_text_dim()));
|
ui.label(egui::RichText::new(summary).size(11.0).color(theme::c_text_dim()));
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
@@ -510,3 +524,65 @@ impl InstallerStep for RescueStep {
|
|||||||
false // Rescue modu her zaman "tamamlanmamış" — normal kuruluma dönmek için rescue_mode=false
|
false // Rescue modu her zaman "tamamlanmamış" — normal kuruluma dönmek için rescue_mode=false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Özet kırpma: Türkçe karakterler 2 bayt olduğu için bayt tabanlı
|
||||||
|
/// dilimleme karakter sınırının ortasına denk gelip panik verirdi.
|
||||||
|
fn truncate(s: &str, max: usize) -> String {
|
||||||
|
let mut out: String = s.chars().take(max).collect();
|
||||||
|
if s.chars().count() > max {
|
||||||
|
out.push('…');
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_handles_multibyte_without_panicking() {
|
||||||
|
// "şğüöç" 5 karakter × 20 = 100 karakter, 200 bayt. 50 karakterlik
|
||||||
|
// kesme bayt sınırının tam ortasına denk gelirdi ve `&s[..50]`
|
||||||
|
// PANİK verirdi.
|
||||||
|
let s = "şğüöç".repeat(20);
|
||||||
|
assert_eq!(s.chars().count(), 100);
|
||||||
|
let t = truncate(&s, 50);
|
||||||
|
assert_eq!(t.chars().count(), 51, "50 karakter + '…'");
|
||||||
|
assert!(t.ends_with('…'));
|
||||||
|
assert!(t.chars().take(50).all(|c| "şğüöç".contains(c)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_leaves_short_strings_untouched() {
|
||||||
|
assert_eq!(truncate("kısa", 50), "kısa");
|
||||||
|
assert_eq!(truncate("", 50), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_history_handles_stray_close_bracket() {
|
||||||
|
// "] Operation ..." — ']' '[' tan önce geliyor. Eski kod
|
||||||
|
// remainder[start+1..end] ile PANİK verirdi.
|
||||||
|
let out = RescueStep::parse_pisi_history("Operation 5 ] bogus: install [vim]");
|
||||||
|
assert_eq!(out.len(), 1, "girdi hâlâ işlenmeli");
|
||||||
|
assert_eq!(out[0].op, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_history_reads_date_and_summary() {
|
||||||
|
let raw = "Operation 42 [2024-03-15 12:00:01]: install [vim, curl]";
|
||||||
|
let out = RescueStep::parse_pisi_history(raw);
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
assert_eq!(out[0].op, 42);
|
||||||
|
assert_eq!(out[0].date, "2024-03-15 12:00:01");
|
||||||
|
assert_eq!(out[0].op_type, "install");
|
||||||
|
// Özet, "]: " sonrasındaki ilk boşluktan itibaren alınır; paket
|
||||||
|
// listesi köşeli parantezleriyle birlikte kalır (mevcut davranış).
|
||||||
|
assert_eq!(out[0].summary, "[vim, curl]");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_history_tolerates_unterminated_bracket() {
|
||||||
|
let out = RescueStep::parse_pisi_history("Operation 7 [2024-03-15 12:00:01: install [x]");
|
||||||
|
assert_eq!(out.len(), 1, "panik vermemeli, kayıt üretmeli");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user