Files
yali-rs/src/jobs/mod.rs
T

2602 lines
99 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Kurulum iş kuyruğu.
//!
//! Mimari:
//! egui (ana iş parçacığı)
//! └─► std::thread::spawn
//! └─► tokio::runtime::Runtime::block_on
//! └─► JobQueue::run_all ─► std::sync::mpsc::Sender<InstallMessage>
//!
//! egui her frame'de `Receiver::try_recv` ile kuyruktan mesaj okur.
//! `egui::Context::request_repaint()` arka plan iş parçacığından çağrılarak
//! UI'nin hemen güncellenmesi sağlanır.
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::time::Instant;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
use rust_i18n::t;
// ─────────────────────────────────────────────
// MESAJ TİPİ
// ─────────────────────────────────────────────
/// Arka plan iş parçacığından UI'ye gönderilen mesajlar.
#[derive(Debug)]
pub enum InstallMessage {
/// Bir log satırı.
Log(String),
/// Genel ilerleme; 0.01.0 arasında.
Progress(f32),
/// Alt ilerleme (ör: rsync içindeki dosya kopyalama); 0.01.0 arasında.
SubProgress(f32),
/// Kopyalanmakta olan dosya adı.
CurrentFile(String),
/// Tüm işler başarıyla tamamlandı.
Done,
/// Bir iş başarısız oldu; kurulum durdu.
Error(String),
}
/// `mpsc::Sender<InstallMessage>` ile `egui::Context` birlikte taşır.
/// Mesaj gönderiminde `request_repaint()` çağrısı UI'yi uyandırır,
/// ancak en fazla saniyede ~30 kez (33ms aralıkla) olacak şekilde
/// throttle edilir. Bu, özellikle rsync gibi yoğun I/O işlemleri
/// sırasında Intel tümleşik GPU'lu (UMA) sistemlerde GPU belleği
/// rekabetini önler ve karakter bozulmasını engeller.
pub struct UiSender {
tx: mpsc::Sender<InstallMessage>,
ctx: eframe::egui::Context,
last_repaint: std::sync::Arc<std::sync::Mutex<Instant>>,
}
impl Clone for UiSender {
fn clone(&self) -> Self {
Self {
tx: self.tx.clone(),
ctx: self.ctx.clone(),
last_repaint: self.last_repaint.clone(),
}
}
}
impl UiSender {
pub fn new(tx: mpsc::Sender<InstallMessage>, ctx: eframe::egui::Context) -> Self {
Self {
tx,
ctx,
last_repaint: std::sync::Arc::new(std::sync::Mutex::new(Instant::now())),
}
}
pub fn send(&self, msg: InstallMessage) {
let _ = self.tx.send(msg);
let mut last = self.last_repaint.lock().unwrap();
if last.elapsed() >= std::time::Duration::from_millis(33) {
self.ctx.request_repaint();
*last = Instant::now();
}
}
pub fn log(&self, s: impl Into<String>) {
self.send(InstallMessage::Log(s.into()));
}
pub fn sub_progress(&self, p: f32) {
self.send(InstallMessage::SubProgress(p));
}
pub fn current_file(&self, f: impl Into<String>) {
self.send(InstallMessage::CurrentFile(f.into()));
}
}
// ─────────────────────────────────────────────
// JOB TRAIT
// ─────────────────────────────────────────────
#[async_trait::async_trait]
pub trait Job: Send + Sync {
fn name(&self) -> &str;
/// İşi asenkron olarak çalıştırır.
/// Hata durumunda `Err(açıklama)` döner; kuyruk durur.
async fn run(&self, ui: &UiSender) -> Result<(), String>;
}
// ─────────────────────────────────────────────
// JOB QUEUE
// ─────────────────────────────────────────────
pub struct JobQueue {
jobs: Vec<Box<dyn Job>>,
}
impl JobQueue {
pub fn new(jobs: Vec<Box<dyn Job>>) -> Self {
Self { jobs }
}
/// Tüm işleri sırayla çalıştırır.
/// İlerleme ve loglar `ui` üzerinden UI'ye iletilir.
pub async fn run_all(&mut self, ui: UiSender) {
let total = self.jobs.len();
for (i, job) in self.jobs.iter().enumerate() {
let localized_name = t!(job.name());
ui.log(format!("{} başlatıldı…", localized_name));
match job.run(&ui).await {
Ok(()) => {
ui.log(format!("{} tamamlandı.", localized_name));
}
Err(e) => {
let msg = format!("{} başarısız: {}", localized_name, e);
ui.log(msg.clone());
ui.send(InstallMessage::Error(msg));
return;
}
}
let progress = (i + 1) as f32 / total as f32;
ui.send(InstallMessage::Progress(progress));
}
ui.send(InstallMessage::Done);
}
}
// ─────────────────────────────────────────────
// SOMUT JOB'LAR
// ─────────────────────────────────────────────
/// Sistem dosyalarını kopyala (rsync).
pub struct CopyFilesJob {
/// Canlı sistemin kök dizini; ör. "/run/livecd/squashfs-root/"
pub source: String,
/// Hedef bağlama noktası; ör. "/mnt"
pub target: String,
}
#[async_trait::async_trait]
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('/') == "";
// ── Önce find ile toplam dosya sayısını bul ──────────
let find_path = self.source.trim_end_matches('/');
let find_path = if find_path.is_empty() { "/" } else { find_path };
let total_files = {
let mut cmd = Command::new("find");
cmd.arg(find_path).arg("-type").arg("f");
if is_root_source {
cmd.arg("-xdev");
}
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::null());
match cmd.spawn() {
Ok(mut c) => {
let stdout = c.stdout.take()
.ok_or_else(|| "find stdout alınamadı".to_string())?;
let r = BufReader::new(stdout);
let mut lines = r.lines();
let mut n: u64 = 0;
while let Ok(Some(_)) = lines.next_line().await {
n += 1;
}
let _ = c.wait().await;
// if n > 0 {
// ui.log(format!("{} dosya bulundu", n));
// }
n
}
Err(_) => 0
}
};
let mut args: Vec<String> = vec![
"-aAXH".to_string(),
"--out-format=%n".to_string(),
"--exclude=/proc/**".to_string(),
"--exclude=/sys/**".to_string(),
"--exclude=/dev/**".to_string(),
"--exclude=/run/**".to_string(),
"--exclude=/tmp/**".to_string(),
"--exclude=/var/tmp/**".to_string(),
"--exclude=/var/cache/**".to_string(),
"--exclude=/home/**".to_string(),
"--exclude=/root/**".to_string(),
"--exclude=/lost+found".to_string(),
"--exclude=*.sqfs".to_string(),
];
// Kaynak canlı sistem köküyse ekstra exclude'ler
if is_root_source {
args.extend_from_slice(&[
"--exclude=/bootmnt/**".to_string(),
"--exclude=/ro_branch/**".to_string(),
"--exclude=/mnt/**".to_string(),
"--exclude=/media/**".to_string(),
"--exclude=/var/log/journal/**".to_string(),
"--exclude=/var/lib/pisi/cache/**".to_string(),
"--exclude=/var/lib/pisi/archives/**".to_string(),
"--exclude=/etc/adjtime".to_string(),
"--exclude=/etc/mtab".to_string(),
"--exclude=/etc/resolv.conf".to_string(),
"--exclude=/etc/hostname".to_string(),
"--exclude=/etc/machine-id".to_string(),
]);
}
if let (Ok(source_path), Ok(target_path)) = (
std::path::Path::new(&self.source).canonicalize(),
std::path::Path::new(&self.target).canonicalize(),
) {
if let Ok(relative_target) = target_path.strip_prefix(&source_path) {
if !relative_target.as_os_str().is_empty() {
args.push(format!("--exclude={}", relative_target.display()));
}
}
}
args.push(self.source.clone());
args.push(self.target.clone());
if DEMO_MODE.load(Ordering::Relaxed) {
ui.log("rsync /run/livecd/squashfs-root/ → /mnt (DEMO)");
let steps = 20;
for i in 0..steps {
let p = (i + 1) as f32 / steps as f32;
ui.current_file(format!("/usr/share/doc/{}/README.md", (i * 137) % 50));
ui.sub_progress(p);
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
}
return Ok(());
}
let mut child = Command::new("rsync")
.args(&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())?;
// Stderr'i arka planda oku (hata birikimi ve tampon taşmasını engelle)
let stderr_buf = std::sync::Arc::new(tokio::sync::Mutex::new(String::new()));
let stderr_buf_clone = stderr_buf.clone();
tokio::spawn(async move {
let r = BufReader::new(stderr);
let mut lines = r.lines();
while let Ok(Some(l)) = lines.next_line().await {
let mut b = stderr_buf_clone.lock().await;
b.push_str(&l);
b.push('\n');
}
});
// Stdout: --out-format=%n ile her satır bir dosya adı
let reader = BufReader::new(stdout);
let mut lines = reader.lines();
let mut file_count: u64 = 0;
// Son UI güncellemesi — çok sık güncelleme GPU (özellikle Intel UMA) baskısını azaltır
let mut last_update = Instant::now();
while let Some(line) = lines.next_line().await
.map_err(|e| format!("rsync çıktısı okunamadı: {}", e))?
{
let trimmed = line.trim().to_string();
if trimmed.is_empty() {
continue;
}
file_count += 1;
let now = Instant::now();
if now.duration_since(last_update) >= std::time::Duration::from_millis(50)
|| file_count == 1
|| trimmed.contains("Packages") || trimmed.contains("package")
{
ui.current_file(trimmed);
last_update = now;
}
if total_files > 0 {
let p = (file_count as f32) / (total_files as f32);
ui.sub_progress(p.min(1.0));
}
}
let status = child.wait().await
.map_err(|e| format!("rsync beklenemedi: {}", e))?;
if !status.success() {
let err = stderr_buf.lock().await.clone();
return Err(format!(
"rsync çıkış kodu: {:?}. Hata: {}",
status.code(),
err.trim()
));
}
Ok(())
}
}
/// /proc, /sys, /dev, /dev/pts dizinlerini chroot hedefine bind-mount yapar.
/// Standart `chroot` için gereklidir; kurulum başında çağrılmalıdır.
pub struct MountBindJob {
pub mount: String, // ör. "/mnt"
}
#[async_trait::async_trait]
impl Job for MountBindJob {
fn name(&self) -> &str { "job_mount_bind" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
let mt = &self.mount;
// (kaynak, hedef) — kaynak sistem üzerindeki gerçek path
let bind_dirs = [
("/proc", format!("{}/proc", mt)),
("/sys", format!("{}/sys", mt)),
("/dev", format!("{}/dev", mt)),
("/dev/pts", format!("{}/dev/pts", mt)),
("/run", format!("{}/run", mt)), // COMAR/dbus için gerekli
];
for (src, target) in &bind_dirs {
tokio::fs::create_dir_all(target).await
.map_err(|e| format!("{} oluşturulamadı: {}", target, e))?;
ui.log(format!("mount --bind {} {}", src, target));
run_cmd("mount", &["--bind", src, target]).await?;
}
// efivarfs — UEFI kurulumunda grub-install için zorunlu
let efivarfs_target = format!("{}/sys/firmware/efi/efivars", mt);
if std::path::Path::new("/sys/firmware/efi/efivars").exists() {
tokio::fs::create_dir_all(&efivarfs_target).await
.map_err(|e| format!("{} oluşturulamadı: {}", efivarfs_target, e))?;
ui.log(format!("mount --bind /sys/firmware/efi/efivars {}", efivarfs_target));
run_cmd("mount", &["--bind", "/sys/firmware/efi/efivars", &efivarfs_target]).await?;
}
Ok(())
}
}
/// Bind-mount'ları temizler (kurulum sonunda çağrılır).
pub struct UnmountBindJob {
pub mount: String,
}
#[async_trait::async_trait]
impl Job for UnmountBindJob {
fn name(&self) -> &str { "job_umount_bind" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
let mt = &self.mount;
// Ters sırada umount — en derin path önce
let subdirs = [
"sys/firmware/efi/efivars",
"dev/pts",
"dev",
"run",
"sys",
"proc",
];
let mut errors: Vec<String> = Vec::new();
for subdir in &subdirs {
let target = format!("{}/{}", mt, subdir);
ui.log(format!("umount {}", target));
match tokio::process::Command::new("umount")
.arg("-lf").arg(&target).status().await
{
Ok(status) if status.success() => {}
Ok(status) => {
// "not mounted" veya "no such file" hataları beklenen durumlar —
// bunları uyarı olarak logla ama kritik hata sayma.
ui.log(format!("⚠ umount uyarısı (kritik değil, kod {}): {}", status, target));
}
Err(e) => {
// Komut çalıştırılamadı — bu gerçek bir sorun.
let msg = format!("umount komutu başlatılamadı ({}): {}", target, e);
ui.log(format!("{}", msg));
errors.push(msg);
}
}
}
if !errors.is_empty() {
return Err(format!(
"Bind mount temizleme hatası — bazı sanal dosya sistemleri bağlı kalmış olabilir:\n{}",
errors.join("\n")
));
}
Ok(())
}
}
/// initramfs yeniler (GRUB öncesi zorunlu).
/// PisiLinux `dracut` kullanıyorsa `dracut -f --regenerate-all`,
/// mkinitcpio kullanıyorsa `mkinitcpio -P`.
pub struct GenerateInitramfsJob {
pub mount: String,
pub encrypt_root: bool,
pub lvm_active: bool,
}
#[async_trait::async_trait]
impl Job for GenerateInitramfsJob {
fn name(&self) -> &str { "job_initramfs" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
// LVM için initramfs'e lvm modülü/hook'u ekleyen konfigürasyon
if self.lvm_active && !self.encrypt_root {
ui.log("LVM algılandı: initramfs LVM ayarlanıyor…");
// dracut: add lvm module
let dracut_conf = format!("{}/etc/dracut.conf.d/99-lvm.conf", self.mount);
let _ = tokio::fs::create_dir_all(format!("{}/etc/dracut.conf.d", self.mount)).await;
let _ = tokio::fs::write(&dracut_conf, "add_dracutmodules+=\" lvm \"\n").await;
// mkinitcpio: add lvm2 hook
// Hem HOOKS="..." hem HOOKS=(... ) sözdizimi desteklenir.
let mkinitcpio_conf = format!("{}/etc/mkinitcpio.conf", self.mount);
if tokio::fs::metadata(&mkinitcpio_conf).await.is_ok() {
if let Ok(content) = tokio::fs::read_to_string(&mkinitcpio_conf).await {
if content.contains("HOOKS=") && !content.contains("lvm2") {
let new_content = content
.replace("HOOKS=\"", "HOOKS=\"lvm2 ")
.replace("HOOKS=(", "HOOKS=(lvm2 ");
let _ = tokio::fs::write(&mkinitcpio_conf, new_content).await;
}
}
}
}
// LUKS için initramfs'e cryptsetup ekleyen konfigürasyon
if self.encrypt_root {
ui.log("LUKS algılandı: initramfs cryptsetup ayarlanıyor…");
// dracut: add crypt module
let dracut_conf = format!("{}/etc/dracut.conf.d/99-luks.conf", self.mount);
tokio::fs::create_dir_all(
format!("{}/etc/dracut.conf.d", self.mount)
).await.map_err(|e| format!("dracut.conf.d oluşturulamadı: {}", e))?;
tokio::fs::write(&dracut_conf, "add_dracutmodules+=\" crypt lvm \"\n")
.await.map_err(|e| format!("dracut luks conf yazılamadı: {}", e))?;
// mkinitcpio: add encrypt hook
// Hem HOOKS="..." hem HOOKS=(... ) sözdizimi desteklenir.
let mkinitcpio_conf = format!("{}/etc/mkinitcpio.conf", self.mount);
if tokio::fs::metadata(&mkinitcpio_conf).await.is_ok() {
let content = tokio::fs::read_to_string(&mkinitcpio_conf)
.await.unwrap_or_default();
if content.contains("HOOKS=") && !content.contains("encrypt") {
let new_content = content
.replace("HOOKS=\"", "HOOKS=\"encrypt ")
.replace("HOOKS=(", "HOOKS=(encrypt ");
let _ = tokio::fs::write(&mkinitcpio_conf, new_content).await;
}
}
}
// PisiLinux /sbin/mkinitramfs, dracut, mkinitcpio sırasıyla dene
let cmds: &[&[&str]] = &[
&["mkinitramfs", "-f"],
&["dracut", "-f", "--regenerate-all"],
&["mkinitcpio", "-P"],
];
for cmd in cmds {
if run_chroot(&self.mount, cmd).await.is_ok() {
ui.log(format!("initramfs oluşturuldu ({})", cmd[0]));
return Ok(());
}
}
Err("initramfs oluşturulamadı: mkinitramfs, dracut, mkinitcpio denendi".to_string())
}
}
/// GRUB bootloader kurulumu (UEFI veya BIOS).
pub struct InstallGrubJob {
pub disk: String,
pub mount: String,
pub is_uefi: bool,
pub boot_device: String,
pub timeout: u32,
pub password: String,
pub kernel_options: String,
pub encrypt_root: bool,
}
#[async_trait::async_trait]
impl Job for InstallGrubJob {
fn name(&self) -> &str { "job_bootloader" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
// Ana dizinleri oluştur (demo modunda rsync çalışmaz)
let etc_default = format!("{}/etc/default", self.mount);
let _ = tokio::fs::create_dir_all(&etc_default).await;
let grub_d = format!("{}/etc/grub.d", self.mount);
let _ = tokio::fs::create_dir_all(&grub_d).await;
// GRUB şifresi (varsa) — grub-mkpasswd-pbkdf2 ile hash üretilir,
// düz metin asla diske yazılmaz.
if !self.password.is_empty() {
ui.log("GRUB şifresi PBKDF2 ile hashleniyor…");
// grub-mkpasswd-pbkdf2 şifreyi stdin'den iki kez okur (şifre + doğrulama)
use tokio::io::AsyncWriteExt;
let pw_input = format!("{}\n{}\n", self.password, self.password);
let mut child = tokio::process::Command::new("grub-mkpasswd-pbkdf2")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| format!("grub-mkpasswd-pbkdf2 başlatılamadı: {}", e))?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(pw_input.as_bytes()).await
.map_err(|e| format!("grub-mkpasswd-pbkdf2 stdin hatası: {}", e))?;
}
let output = child.wait_with_output().await
.map_err(|e| format!("grub-mkpasswd-pbkdf2 bekleme hatası: {}", e))?;
// Çıktıdan "grub.pbkdf2.sha512...." token'ını ayrıştır
let stdout = String::from_utf8_lossy(&output.stdout);
let hash = stdout
.lines()
.find_map(|l| {
let pos = l.find("grub.pbkdf2")?;
Some(l[pos..].trim().to_string())
})
.ok_or("grub-mkpasswd-pbkdf2 çıktısında hash bulunamadı")?;
let custom_grub = format!(
"#!/bin/sh\nset superusers=\"root\"\npassword_pbkdf2 root {}\n",
hash
);
let pw_path = format!("{}/etc/grub.d/40_custom", self.mount);
tokio::fs::write(&pw_path, custom_grub)
.await
.map_err(|e| format!("GRUB şifre dosyası yazılamadı: {}", e))?;
run_cmd("chmod", &["+x", &pw_path]).await?;
ui.log("GRUB şifresi PBKDF2 hash olarak yazıldı.");
}
// GRUB timeout ayarı
ui.log(format!("GRUB timeout ayarlanıyor: {}s", self.timeout));
let default_path = format!("{}/etc/default/grub", self.mount);
let mut grub_default = tokio::fs::read_to_string(&default_path)
.await
.unwrap_or_default();
// timeout satırını bul/değiştir
let timeout_line = format!("GRUB_TIMEOUT={}", self.timeout);
let found_timeout = grub_default.lines()
.any(|l| l.trim_start().starts_with("GRUB_TIMEOUT="));
if found_timeout {
let mut lines: Vec<String> = grub_default.lines()
.map(|l| {
if l.trim_start().starts_with("GRUB_TIMEOUT=") {
timeout_line.clone()
} else {
l.to_string()
}
})
.collect();
lines.push(String::new());
grub_default = lines.join("\n");
} else {
grub_default.push_str(&format!("\n{}\n", timeout_line));
}
// kernel cmdline
let mut cmdline = self.kernel_options.clone();
if self.encrypt_root {
// Kök bölüm UUID'sini blkid'den al (LUKS başlık UUID'si)
let (root_num, _swap_num): (u32, u32) = if self.is_uefi { (3, 2) } else { (2, 1) };
let root_part = part_path(&self.disk, root_num);
let root_uuid = blkid_uuid(&root_part).await.unwrap_or_default();
let crypt_arg = format!("cryptdevice=UUID={}:cryptroot root=/dev/mapper/cryptroot", root_uuid);
if cmdline.is_empty() {
cmdline = crypt_arg;
} else {
cmdline = format!("{} {}", crypt_arg, cmdline);
}
}
if !cmdline.is_empty() {
let cmdline_line = format!("GRUB_CMDLINE_LINUX=\"{}\"", cmdline);
let found_cmdline = grub_default.lines()
.any(|l| l.trim_start().starts_with("GRUB_CMDLINE_LINUX="));
if found_cmdline {
let mut lines: Vec<String> = grub_default.lines()
.map(|l| {
if l.trim_start().starts_with("GRUB_CMDLINE_LINUX=") {
cmdline_line.clone()
} else {
l.to_string()
}
})
.collect();
lines.push(String::new());
grub_default = lines.join("\n");
} else {
grub_default.push_str(&format!("{}\n", cmdline_line));
}
}
tokio::fs::write(&default_path, grub_default)
.await
.map_err(|e| format!("/etc/default/grub yazılamadı: {}", e))?;
// Chroot içinde grub2-install çalıştır (UEFI veya BIOS)
let mut grub_args = vec!["grub2-install"];
let efi_dir_arg;
let disk_arg;
if self.is_uefi {
ui.log("GRUB UEFI modu kuruluyor (chroot içinde)…");
efi_dir_arg = "--efi-directory=/boot/efi".to_string();
grub_args.extend_from_slice(&[
"--target=x86_64-efi",
&efi_dir_arg,
"--bootloader-id=PisiLinux",
"--recheck",
]);
} else {
let install_dev = if self.boot_device.is_empty() { &self.disk } else { &self.boot_device };
ui.log(format!("GRUB BIOS modu {} aygıtına kuruluyor (chroot içinde)…", install_dev));
disk_arg = install_dev.clone();
grub_args.extend_from_slice(&[
"--target=i386-pc",
&disk_arg,
"--recheck",
]);
}
run_chroot(&self.mount, &grub_args).await?;
ui.log("grub2-mkconfig çalıştırılıyor…");
run_chroot(&self.mount, &[
"grub2-mkconfig", "-o", "/boot/grub2/grub.cfg",
]).await?;
Ok(())
}
}
// ─────────────────────────────────────────────
// YARDIMCI: KOMUT ÇALIŞTIRICI
// ─────────────────────────────────────────────
static DEMO_MODE: AtomicBool = AtomicBool::new(false);
/// Disk adı ve bölüm numarasından tam aygıt yolunu oluşturur.
///
/// - SATA/SCSI: `/dev/sda` + 1 → `/dev/sda1`
/// - NVMe: `/dev/nvme0n1` + 1 → `/dev/nvme0n1p1`
/// - MMC: `/dev/mmcblk0` + 1 → `/dev/mmcblk0p1`
fn part_path(disk: &str, num: u32) -> String {
crate::installer::part_path(disk, num)
}
/// Başarısızlık durumunda stderr'i hata mesajı olarak döner.
async fn run_cmd(program: &str, args: &[&str]) -> Result<(), String> {
if DEMO_MODE.load(Ordering::Relaxed) {
tokio::time::sleep(std::time::Duration::from_millis(3000)).await;
return Ok(());
}
let output = Command::new(program)
.args(args)
.output()
.await
.map_err(|e| format!("{} çalıştırılamadı: {}", program, e))?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
Err(if stderr.is_empty() { stdout } else { stderr })
}
}
/// `parted` komutlarını çalıştırmak için özel sarmalayıcı.
///
/// `parted -s` canlı sistemde kullanımda olan bir disk üzerinde bölüm
/// tablosunu kernel'e bildiremeyince exit 1 döndürür; ancak değişiklikler
/// diske **yazılmıştır** — kernel güncellemesi `sync_partition_table` ile
/// yapılır. Bu fonksiyon yalnızca gerçekten kritik hataları (disk bulunamadı
/// vb.) yakalar; "kernel haberdar edilemedi" uyarısını yok sayar.
async fn run_parted(args: &[&str]) -> Result<(), String> {
if DEMO_MODE.load(Ordering::Relaxed) {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
return Ok(());
}
let output = Command::new("parted")
.args(args)
.output()
.await
.map_err(|e| format!("parted çalıştırılamadı: {}", e))?;
if output.status.success() {
return Ok(());
}
// Parted, kernel güncelleyemediğinde exit 1 verir ama disk yazımı
// başarılıdır. Bu spesifik uyarıyı hata saymıyoruz.
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{}{}", stderr, stdout);
let is_kernel_notify_warning = combined.contains("kernel değişiklikten haberdar edilemedi")
|| combined.contains("kernel was not notified")
|| combined.contains("unable to inform the kernel")
|| combined.contains("still in use");
if is_kernel_notify_warning {
// Uyarıyı yok say; sync_partition_table kernel'i güncelleyecek
return Ok(());
}
// Gerçek bir hata
Err(combined.trim().to_string())
}
/// Standart `chroot` ile verilen komutu hedef dizinde çalıştırır.
/// Bind-mount'ların önceden yapılmış olduğu varsayılır (MountBindJob).
async fn run_chroot(mount: &str, cmd: &[&str]) -> Result<(), String> {
if DEMO_MODE.load(Ordering::Relaxed) {
tokio::time::sleep(std::time::Duration::from_millis(2000)).await;
return Ok(());
}
// chroot <mount> <cmd[0]> [cmd[1..]...]
let mut args = vec![mount];
args.extend_from_slice(cmd);
let output = Command::new("chroot")
.args(&args)
.output()
.await
.map_err(|e| format!("chroot çalıştırılamadı: {}", e))?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
Err(if stderr.is_empty() { stdout } else { stderr })
}
}
/// LUKS bölümünü güvenli şekilde formatlar.
///
/// Şifre, `sh -c` + string interpolasyon yerine doğrudan `stdin` pipe'ı
/// üzerinden iletilir; böylece shell injection saldırıları önlenir.
async fn run_luks_format(device: &str, password: &str) -> Result<(), String> {
if DEMO_MODE.load(Ordering::Relaxed) {
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
return Ok(());
}
use tokio::io::AsyncWriteExt;
let mut child = tokio::process::Command::new("cryptsetup")
.args([
"luksFormat", "-q", "--type", "luks2",
"--key-file", "-",
device,
])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| format!("cryptsetup luksFormat başlatılamadı: {}", e))?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(password.as_bytes()).await
.map_err(|e| format!("cryptsetup stdin yazma hatası: {}", e))?;
}
let output = child.wait_with_output().await
.map_err(|e| format!("cryptsetup bekleme hatası: {}", e))?;
if output.status.success() {
Ok(())
} else {
Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
}
}
/// LUKS bölümünü güvenli şekilde açar (open).
///
/// Şifre, shell interpolasyon yerine `stdin` pipe'ı üzerinden iletilir.
async fn run_luks_open(device: &str, name: &str, password: &str) -> Result<(), String> {
if DEMO_MODE.load(Ordering::Relaxed) {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
return Ok(());
}
use tokio::io::AsyncWriteExt;
let mut child = tokio::process::Command::new("cryptsetup")
.args(["open", "--key-file", "-", device, name])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| format!("cryptsetup open başlatılamadı: {}", e))?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(password.as_bytes()).await
.map_err(|e| format!("cryptsetup open stdin yazma hatası: {}", e))?;
}
let output = child.wait_with_output().await
.map_err(|e| format!("cryptsetup open bekleme hatası: {}", e))?;
if output.status.success() {
Ok(())
} else {
Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
}
}
/// Bölümlendirme öncesi diski hazırlar:
/// mevcut bağlamaları ve swap'ı ayırır, ardından kernel'e yeni
/// bölüm tablosunu duyurarak aygıt düğümlerinin hazır olmasını bekler.
async fn prepare_disk(disk: &str, ui: &UiSender) {
if DEMO_MODE.load(Ordering::Relaxed) { return; }
// 1. Diskteki aktif swap'ları kapat
ui.log(format!("Swap ayrılıyor: {}", disk));
let lsblk = tokio::process::Command::new("lsblk")
.args(["-o", "NAME,TYPE", "-l", "--noheadings", disk])
.output().await;
if let Ok(out) = lsblk {
let text = String::from_utf8_lossy(&out.stdout);
for line in text.lines() {
if line.contains("part") {
if let Some(name) = line.split_whitespace().next() {
let dev = format!("/dev/{}", name);
let _ = tokio::process::Command::new("swapoff")
.arg(&dev).status().await;
}
}
}
}
// 2. Disk bölümlerini umount et (hata görmezden gelinir)
ui.log(format!("Bağlantılar ayrılıyor: {}", disk));
for n in 1..=32u32 {
let part = crate::installer::part_path(disk, n);
if !std::path::Path::new(&part).exists() { break; }
let _ = tokio::process::Command::new("umount")
.args(["-lf", &part]).status().await;
}
// 3. İlk senkronizasyon
let _ = tokio::process::Command::new("partprobe")
.arg(disk).status().await;
let _ = tokio::process::Command::new("udevadm")
.args(["settle", "--timeout=10"]).status().await;
}
/// Bölümlendirme sonrası kernel'e yeni tabloyu bildirir ve aygıt
/// düğümlerinin hazır olmasını bekler. `parted`'ın uyarı vermesi
/// normaldir; bu fonksiyon hata döndürmez.
async fn sync_partition_table(disk: &str) {
if DEMO_MODE.load(Ordering::Relaxed) { return; }
let _ = tokio::process::Command::new("partprobe")
.arg(disk).status().await;
let _ = tokio::process::Command::new("blockdev")
.args(["--rereadpt", disk]).status().await;
let _ = tokio::process::Command::new("udevadm")
.args(["settle", "--timeout=10"]).status().await;
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
/// ISO ortamında canlı sistemin kaynak dizinini otomatik tespit eder.
///
/// Sırasıyla:
/// 1. `/proc/mounts` içinde squashfs/overlay mount noktalarını tara
/// 2. Sabit aday listesini dene (her adayda bin/ + etc/ varlığı aranır)
/// 3. Hiçbiri bulunamazsa `/` (canlı sistem kökü) kullan — rsync
/// zaten /proc /sys /dev /run'u exclude eder.
fn has_root_fs(path: &std::path::Path) -> bool {
(path.join("bin").exists() || path.join("usr/bin").exists()) && path.join("etc").exists()
}
fn is_iso_mount(path: &str) -> bool {
let sqfs_check = [
"pisi/pisi.sqfs",
"live/pisi.sqfs",
"live/filesystem.squashfs",
"casper/filesystem.squashfs",
"live squashfs",
];
for pattern in &sqfs_check {
if std::path::Path::new(path).join(pattern).exists() {
return true;
}
}
false
}
pub fn detect_source_dir() -> String {
// 1. /proc/mounts içinde squashfs veya overlay mount noktalarını tara
if let Ok(mounts) = std::fs::read_to_string("/proc/mounts") {
for line in mounts.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 3 { continue; }
let fs_type = parts[2];
let mount_pt = parts[1];
match fs_type {
"squashfs" => {
if mount_pt != "/" {
let p = std::path::Path::new(mount_pt);
if has_root_fs(p) {
return format!("{}/", mount_pt.trim_end_matches('/'));
}
}
}
"overlay" => {
// lowerdir parametresini ayıkla
let opts = parts.get(3).unwrap_or(&"");
for opt in opts.split(',') {
if let Some(lower) = opt.strip_prefix("lowerdir=") {
for dir in lower.split(':') {
let p = std::path::Path::new(dir);
if p.exists() && has_root_fs(p) {
return format!("{}/", dir.trim_end_matches('/'));
}
}
}
}
}
_ => {}
}
}
}
// 2. Sabit aday listesi (her adayda bin/ + etc/ kontrolü)
// NOT: /bootmnt ISO kökü olabilir; altında pisi.sqfs varsa atlanır.
for path in &[
"/run/livecd/squashfs-root",
"/run/live/rootfs",
"/run/live/medium",
"/lib/live/mount/rootfs",
"/mnt/sqfs",
"/mnt/livecd",
"/mnt/cdrom",
"/bootmnt/live",
"/bootmnt/livecd",
] {
let p = std::path::Path::new(path);
if p.exists() && has_root_fs(p) {
return format!("{}/", path);
}
}
// 2b. /bootmnt son çare; ama ISO köküyse (pisi.sqfs varsa) atla
let bootmnt = std::path::Path::new("/bootmnt");
if bootmnt.exists() && !is_iso_mount("/bootmnt") && has_root_fs(bootmnt) {
return "/bootmnt/".to_string();
}
// 3. Hiçbiri bulunamazsa canlı sistem kökü
"/".to_string()
}
/// Kurulum kuyruğu için ortak yapılandırma.
pub struct JobQueueConfig {
pub mount: String,
pub source: String,
pub locale: String,
pub timezone: String,
pub username: String,
pub password: String,
pub hostname: String,
pub kb_layout: String,
pub kb_variant: String,
pub demo_mode: bool,
pub boot_device: String,
pub bootloader_timeout: u32,
pub bootloader_password: String,
pub kernel_options: String,
pub use_ntp: bool,
pub manual_year: i32,
pub manual_month: u32,
pub manual_day: u32,
pub manual_hour: u32,
pub manual_minute: u32,
pub root_password: String,
pub display_manager: String,
pub autologin: bool,
pub desktop_environment: String,
pub selected_package_groups: Vec<String>,
}
// ─────────────────────────────────────────────
// FABRİKA: STANDART KURULUM KUYRUĞU
// ─────────────────────────────────────────────
// ─────────────────────────────────────────────
// PARTITION JOB (otomatik bölümleme)
// ─────────────────────────────────────────────
use crate::installer::{PartitionPlan, PartitionTableType};
/// `parted` ve `mkfs` araçlarını kullanarak diski otomatik bölümlendirir.
/// Plan, PartitionStep'te hesaplanmış ve GlobalState'te saklanmıştır.
pub struct PartitionJob {
pub plan: PartitionPlan,
}
#[async_trait::async_trait]
impl Job for PartitionJob {
fn name(&self) -> &str { "job_partition" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
let disk = &self.plan.disk;
let is_gpt = self.plan.table_type == PartitionTableType::Gpt;
// 0. Diski hazırla: bağlamaları ayır, kernel tablosunu temizle
prepare_disk(disk, ui).await;
// 1. Bölüm tablosu oluştur
let table_label = if is_gpt { "gpt" } else { "msdos" };
ui.log(format!("parted {} mktable {}", disk, table_label));
run_parted(&["-s", disk, "mktable", table_label]).await?;
let mut next_mb: u64 = 1; // İlk MB'ı boot kayıtları için bırak
// 2. EFI bölümü (yalnızca GPT/UEFI)
if self.plan.efi_mb > 0 {
let end_mb = next_mb + self.plan.efi_mb;
ui.log(format!("EFI bölümü oluşturuluyor: {}{} MB", next_mb, end_mb));
run_parted(&[
"-s", disk, "mkpart", "fat32",
&format!("{}MB", next_mb), &format!("{}MB", end_mb),
]).await?;
run_parted(&["-s", disk, "set", "1", "esp", "on"]).await?;
next_mb = end_mb;
}
// 3. Swap bölümü
let swap_end = next_mb + self.plan.swap_mb;
let swap_num: u32 = if self.plan.efi_mb > 0 { 2 } else { 1 };
ui.log(format!("Swap bölümü oluşturuluyor: {}{} MB", next_mb, swap_end));
run_parted(&[
"-s", disk, "mkpart", "linux-swap",
&format!("{}MB", next_mb), &format!("{}MB", swap_end),
]).await?;
next_mb = swap_end;
// 4. Kök bölümü (kalan alan)
let root_num: u32 = if self.plan.efi_mb > 0 { 3 } else { 2 };
ui.log(format!("Kök bölümü oluşturuluyor: {}MB disk sonu", next_mb));
run_parted(&[
"-s", disk, "mkpart", "ext4",
&format!("{}MB", next_mb), "100%",
]).await?;
// 4.5. Tüm mkpart bitti; kernel'e bildir ve aygıt düğümlerini bekle
sync_partition_table(disk).await;
// 5. Biçimlendirme
let efi_part = part_path(disk, 1);
let swap_part = part_path(disk, swap_num);
let root_part = part_path(disk, root_num);
if self.plan.efi_mb > 0 {
ui.log(format!("mkfs.fat -F32 {}", efi_part));
run_cmd("mkfs.fat", &["-F32", &efi_part]).await?;
}
ui.log(format!("mkswap {}", swap_part));
run_cmd("mkswap", &[&swap_part]).await?;
ui.log(format!("swapon {}", swap_part));
run_cmd("swapon", &[&swap_part]).await?;
// LUKS şifreleme (kök bölümü)
// Şifre, shell injection'ı önlemek için doğrudan stdin pipe üzerinden
// iletilir; `sh -c` + string interpolasyon kullanılmaz.
let root_device = if self.plan.encrypt_root && !self.plan.luks_password.is_empty() {
let luks_name = "cryptroot";
ui.log(format!("cryptsetup luksFormat {}", root_part));
run_luks_format(&root_part, &self.plan.luks_password).await
.map_err(|e| format!("LUKS format hatası: {}", e))?;
ui.log(format!("cryptsetup open {} {}", root_part, luks_name));
run_luks_open(&root_part, luks_name, &self.plan.luks_password).await
.map_err(|e| format!("LUKS open hatası: {}", e))?;
format!("/dev/mapper/{}", luks_name)
} else {
root_part.clone()
};
ui.log(format!("mkfs.ext4 -F {}", root_device));
run_cmd("mkfs.ext4", &["-F", &root_device]).await?;
Ok(())
}
}
/// Bölüm yolunu bağla.
pub struct MountPartitionsJob {
pub plan: PartitionPlan,
pub mount: String,
}
#[async_trait::async_trait]
impl Job for MountPartitionsJob {
fn name(&self) -> &str { "job_mount" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
let disk = &self.plan.disk;
let mount = &self.mount;
let root_num: u32 = if self.plan.efi_mb > 0 { 3 } else { 2 };
let root_part = part_path(disk, root_num);
let root_device = if self.plan.encrypt_root {
"/dev/mapper/cryptroot".to_string()
} else {
root_part.clone()
};
ui.log(format!("mount {} {}", root_device, mount));
run_cmd("mount", &[&root_device, mount]).await?;
if self.plan.efi_mb > 0 {
let efi_dir = format!("{}/boot/efi", mount);
ui.log(format!("mkdir -p {}", efi_dir));
tokio::fs::create_dir_all(&efi_dir).await
.map_err(|e| format!("{} oluşturulamadı: {}", efi_dir, e))?;
let efi_part = part_path(disk, 1);
ui.log(format!("mount {} {}", efi_part, efi_dir));
run_cmd("mount", &[&efi_part, &efi_dir]).await?;
}
Ok(())
}
}
/// Tam kurulum iş kuyruğu.
///
/// Sıra:
/// 1. Disk bölümlendirme (parted + mkfs)
/// 2. Bölümleri bağlama
/// 3. /proc /sys /dev bind-mount (chroot hazırlığı)
/// 4. Sistem dosyalarını kopyalama (rsync)
/// 5. UUID'leri oku → fstab yaz
/// 6. Hostname + /etc/hosts
/// 7. Locale yapılandırması + locale-gen
/// 8. Zaman dilimi + hwclock
/// 9. Klavye (vconsole + X11)
/// 10. Kullanıcı oluşturma + şifre (chpasswd)
/// 11. initramfs yenile (mkinitramfs / dracut / mkinitcpio)
/// 12. GRUB kurulumu (chroot içinde)
/// 13. COMAR / pisi configure-pending
/// 14. ldconfig
/// 15. update-environment (PisiLinux init)
/// 16. Live ortam temizliği
/// 17. Bind-mount'ları temizle
pub fn build_full_job_queue(
plan: PartitionPlan,
cfg: JobQueueConfig,
) -> JobQueue {
DEMO_MODE.store(cfg.demo_mode, std::sync::atomic::Ordering::Relaxed);
let is_uefi = plan.efi_mb > 0;
let encrypt_root = plan.encrypt_root;
let disk = plan.disk.clone();
let mt = cfg.mount.clone();
let plan2 = plan.clone();
let plan3 = plan.clone();
// Kaynak dizini: parametre boşsa otomatik tespit et
let src = if cfg.source.is_empty() { detect_source_dir() } else { cfg.source };
let mut jobs: Vec<Box<dyn Job>> = vec![
// 1. Bölümlendir
Box::new(PartitionJob { plan }),
// 2. Bağla
Box::new(MountPartitionsJob { plan: plan2, mount: mt.clone() }),
// 3. /proc /sys /dev bind-mount
Box::new(MountBindJob { mount: mt.clone() }),
// 4. Kopyala
Box::new(CopyFilesJob { source: src, target: mt.clone() }),
// 5. UUID oku + fstab yaz
Box::new(FstabFromBlkidJob { plan: plan3, mount: mt.clone() }),
// 6. Hostname
Box::new(ConfigureHostnameJob {
mount: mt.clone(),
hostname: cfg.hostname.clone(),
}),
// 7. Locale
Box::new(ConfigureLocaleJob {
mount: mt.clone(),
locale: cfg.locale.clone(),
}),
// 8. Zaman dilimi
Box::new(ConfigureTimezoneJob {
mount: mt.clone(),
timezone: cfg.timezone.clone(),
}),
// 9. Tarih/Saat (NTP)
Box::new(ConfigureDateTimeJob {
mount: mt.clone(),
use_ntp: cfg.use_ntp,
year: cfg.manual_year,
month: cfg.manual_month,
day: cfg.manual_day,
hour: cfg.manual_hour,
minute: cfg.manual_minute,
}),
// 10. Klavye
Box::new(ConfigureKeyboardJob {
mount: mt.clone(),
layout: cfg.kb_layout.clone(),
variant: cfg.kb_variant.clone(),
}),
// 11. Kullanıcı
Box::new(CreateUserJob {
mount: mt.clone(),
username: cfg.username.clone(),
password: cfg.password.clone(),
root_password: cfg.root_password.clone(),
}),
// 12. initramfs
Box::new(GenerateInitramfsJob { mount: mt.clone(), encrypt_root, lvm_active: false }),
];
// 13. Bootloader (GRUB)
jobs.push(Box::new(InstallGrubJob {
disk: disk.clone(),
mount: mt.clone(),
is_uefi,
boot_device: cfg.boot_device.clone(),
timeout: cfg.bootloader_timeout,
password: cfg.bootloader_password.clone(),
kernel_options: cfg.kernel_options.clone(),
encrypt_root,
}));
// 14. Ekran yöneticisi
jobs.push(Box::new(ConfigureDisplayManagerJob {
mount: mt.clone(),
display_manager: cfg.display_manager.clone(),
autologin: cfg.autologin,
username: cfg.username.clone(),
desktop_environment: cfg.desktop_environment.clone(),
}));
// 15. COMAR / pisi configure-pending
jobs.push(Box::new(RunComarJob { mount: mt.clone() }));
// 16. ldconfig
jobs.push(Box::new(RunLdconfigJob { mount: mt.clone() }));
// 17. update-environment (PisiLinux init yapılandırması)
jobs.push(Box::new(UpdateEnvironmentJob { mount: mt.clone() }));
// 18. Ek paket grupları (netinstall)
if !cfg.selected_package_groups.is_empty() {
jobs.push(Box::new(InstallExtraPackagesJob {
mount: mt.clone(),
package_groups: cfg.selected_package_groups.clone(),
}));
}
// 19. Live ortam izlerini temizle
jobs.push(Box::new(CleanupLiveJob { mount: mt.clone() }));
// 20. Bind-mount temizle
jobs.push(Box::new(UnmountBindJob { mount: mt.clone() }));
JobQueue::new(jobs)
}
/// Ekran yöneticisi (SDDM/LightDM/GDM) yapılandırması.
pub struct ConfigureDisplayManagerJob {
pub mount: String,
pub display_manager: String,
pub autologin: bool,
pub username: String,
pub desktop_environment: String,
}
#[async_trait::async_trait]
impl Job for ConfigureDisplayManagerJob {
fn name(&self) -> &str { "job_display_manager" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
let dm = &self.display_manager;
ui.log(format!("Ekran yöneticisi yapılandırılıyor: {}", dm));
match dm.as_str() {
"sddm" => {
let conf = format!("{}/usr/lib/sddm/sddm.conf.d/sddm.conf", self.mount);
let session = match self.desktop_environment.as_str() {
"plasma" => "plasmax11.desktop",
"xfce" => "xfce.desktop",
"gnome" => "gnome.desktop",
"cinnamon" => "cinnamon.desktop",
"budgie" => "budgie-desktop.desktop",
"lxde" => "LXDE.desktop",
"lxqt" => "lxqt.desktop",
"mate" => "mate.desktop",
"lumina" => "lumina-desktop.desktop",
_ => "plasmax11.desktop",
};
let content = if self.autologin {
format!(
"[Autologin]\nUser={}\nSession={}\n\n[Theme]\nCurrent=pisilinux24\n",
self.username, session
)
} else {
"[Autologin]\nUser=\nSession=\n\n[Theme]\nCurrent=pisilinux24\n".to_string()
};
tokio::fs::write(&conf, content)
.await
.map_err(|e| format!("sddm.conf yazılamadı: {}", e))?;
}
"lightdm" => {
let conf = format!("{}/etc/lightdm/lightdm.conf", self.mount);
// Mevcut dosyayı oku veya baştan yaz
let mut content = String::new();
if let Ok(existing) = tokio::fs::read_to_string(&conf).await {
content = existing;
}
if self.autologin {
let auto_lines = format!(
"[Seat:*]\nautologin-user={}\nautologin-session={}\n",
self.username, self.desktop_environment
);
content.push_str(&auto_lines);
}
tokio::fs::write(&conf, content)
.await
.map_err(|e| format!("lightdm.conf yazılamadı: {}", e))?;
}
"gdm" => {
let dir = format!("{}/etc/gdm", self.mount);
let _ = tokio::fs::create_dir_all(&dir).await;
let conf = format!("{}/etc/gdm/custom.conf", self.mount);
let session = match self.desktop_environment.as_str() {
"plasma" => "plasmax11.desktop",
"xfce" => "xfce.desktop",
"gnome" => "gnome.desktop",
"cinnamon" => "cinnamon.desktop",
"budgie" => "budgie-desktop.desktop",
"lxde" => "LXDE.desktop",
"lxqt" => "lxqt.desktop",
"mate" => "mate.desktop",
"lumina" => "lumina-desktop.desktop",
_ => "gnome.desktop",
};
let content = if self.autologin {
format!(
"[daemon]\nAutomaticLoginEnable=true\nAutomaticLogin={}\nDefaultSession={}\n",
self.username, session
)
} else {
"[daemon]\n".to_string()
};
tokio::fs::write(&conf, content)
.await
.map_err(|e| format!("gdm/custom.conf yazılamadı: {}", e))?;
}
_ => {
ui.log(format!("⚠ Bilinmeyen ekran yöneticisi: {}", dm));
}
}
ui.log("✓ Ekran yöneticisi yapılandırması tamamlandı");
Ok(())
}
}
/// Ek paket gruplarını chroot içinde kurar.
pub struct InstallExtraPackagesJob {
pub mount: String,
pub package_groups: Vec<String>,
}
#[async_trait::async_trait]
impl Job for InstallExtraPackagesJob {
fn name(&self) -> &str { "job_netinstall" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
use crate::steps::netinstall::PACKAGE_GROUPS;
let mut packages: Vec<&str> = Vec::new();
for (key, _, pkgs) in PACKAGE_GROUPS {
if self.package_groups.contains(&key.to_string()) {
packages.extend_from_slice(pkgs);
}
}
if packages.is_empty() {
ui.log("Ek paket seçilmedi, atlanıyor.");
return Ok(());
}
let pkg_list = packages.join(" ");
ui.log(format!("Ek paketler kuruluyor: {}", pkg_list));
match run_chroot(&self.mount, &["pisi", "install", &pkg_list]).await {
Ok(_) => ui.log("✓ Ek paketler kuruldu"),
Err(e) => {
ui.log(format!("⚠ pisi install uyarısı: {}", e));
ui.log("⚠ Bu hata kritik değilse kurulum devam edecek.");
}
}
Ok(())
}
}
/// COMAR servislerini ve tetikleyicilerini yapılandırır.
pub struct RunComarJob {
pub mount: String,
}
#[async_trait::async_trait]
impl Job for RunComarJob {
fn name(&self) -> &str { "job_comar" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
// pisi configure-pending: paket post-install scriptlerini çalıştırır.
// Yapılandırılacak paket yoksa çıkış kodu sıfır dışı olabilir — fatal değil.
ui.log("pisi configure-pending çalıştırılıyor (COMAR)…");
match run_chroot(&self.mount, &["pisi", "configure-pending"]).await {
Ok(_) => {
ui.log("✓ pisi configure-pending tamamlandı");
}
Err(e) => {
// "no packages to configure" gibi durumlar hata değil
if e.contains("no packages") || e.contains("Nothing to do") {
ui.log("⚠ configure-pending: yapılandırılacak paket yok, atlanıyor");
} else {
// Gerçek hata — uyar ama kurulumu durdurma
ui.log(format!("⚠ pisi configure-pending uyarısı: {}", e));
ui.log("⚠ Bu uyarı kritik değilse kurulum devam edecek.");
}
}
}
Ok(())
}
}
/// ldconfig: paylaşımlı kütüphane önbelleğini yeniden oluşturur.
pub struct RunLdconfigJob {
pub mount: String,
}
#[async_trait::async_trait]
impl Job for RunLdconfigJob {
fn name(&self) -> &str { "job_ldconfig" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
ui.log("ldconfig çalıştırılıyor…");
match run_chroot(&self.mount, &["ldconfig"]).await {
Ok(_) => ui.log("✓ ldconfig tamamlandı"),
Err(e) => ui.log(format!("⚠ ldconfig uyarısı: {}", e)),
}
Ok(())
}
}
/// update-environment: PisiLinux init/betiklerini yeniden yapılandırır.
pub struct UpdateEnvironmentJob {
pub mount: String,
}
#[async_trait::async_trait]
impl Job for UpdateEnvironmentJob {
fn name(&self) -> &str { "job_update_env" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
ui.log("update-environment çalıştırılıyor…");
match run_chroot(&self.mount, &["update-environment"]).await {
Ok(_) => ui.log("✓ update-environment tamamlandı"),
Err(e) => ui.log(format!("⚠ update-environment uyarısı: {}", e)),
}
Ok(())
}
}
/// Live ortam izlerini hedef sistemden temizler.
pub struct CleanupLiveJob {
pub mount: String,
}
#[async_trait::async_trait]
impl Job for CleanupLiveJob {
fn name(&self) -> &str { "job_cleanup_live" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
let mt = &self.mount;
ui.log("Live ortam izleri temizleniyor…");
// run/livemedia
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;
// 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-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, &["pisi", "rm", "yali-rs"]).await;
// live paket deposunu kaldır
let _ = run_chroot(&self.mount, &["pisi", "rr", "live"]).await;
// depolarları yeniden ekle (stable2 + contrib)
ui.log("Depolar Ekleniyor…");
let _ = run_chroot(&self.mount, &["pisi", "ar", "stable2", "https://stable2.pisilinux.org/pisi-index.xml.xz"]).await;
let _ = run_chroot(&self.mount, &["pisi", "ar", "contrib", "https://contrib.pisilinux.org/pisi-index.xml.xz"]).await;
// depoları aktif et
ui.log("Depolar aktif ediliyor...");
let _ = run_chroot(&self.mount, &["pisi", "er", "contrib", "stable2"]).await;
// paket listelerini güncelle
ui.log("Depolar Güncelleniyor...");
let _ = run_chroot(&self.mount, &["pisi", "ur"]).await;
ui.log("✓ Live temizlik tamamlandı");
Ok(())
}
}
/// blkid ile UUID okuyup fstab'ı doğrudan yazan job.
/// (async closure Rust'ta trait object olarak kullanılamaz;
/// bu nedenle ayrı bir Job struct'ı gerekli.)
struct FstabFromBlkidJob {
plan: PartitionPlan,
mount: String,
}
#[async_trait::async_trait]
impl Job for FstabFromBlkidJob {
fn name(&self) -> &str { "job_fstab" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
ui.log("blkid ile UUID'ler okunuyor…");
let (root_uuid, efi_uuid, swap_uuid) = read_partition_uuids(&self.plan).await?;
ui.log(format!("Kök UUID: {}", root_uuid));
let job = GenerateFstabJob {
mount: self.mount.clone(),
root_uuid,
efi_uuid,
swap_uuid,
encrypt_root: self.plan.encrypt_root,
};
job.run(ui).await
}
}
// ─────────────────────────────────────────────
// CHROOT YAPILANDIRMA JOB'LARI
// ─────────────────────────────────────────────
/// /etc/hostname ve /etc/hosts dosyalarını yazar.
pub struct ConfigureHostnameJob {
pub mount: String,
pub hostname: String,
}
#[async_trait::async_trait]
impl Job for ConfigureHostnameJob {
fn name(&self) -> &str { "job_hostname" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
let etc_dir = format!("{}/etc", self.mount);
let _ = tokio::fs::create_dir_all(&etc_dir).await;
// /etc/hostname
let hostname_path = format!("{}/etc/hostname", self.mount);
ui.log(format!("Yazılıyor: {}", hostname_path));
tokio::fs::write(&hostname_path, format!("{}\n", self.hostname))
.await
.map_err(|e| format!("/etc/hostname yazılamadı: {}", e))?;
// /etc/hosts
let hosts_path = format!("{}/etc/hosts", self.mount);
let hosts_content = format!(
"127.0.0.1\tlocalhost\n\
::1\t\tlocalhost\n\
127.0.1.1\t{hostname}\n",
hostname = self.hostname
);
ui.log(format!("Yazılıyor: {}", hosts_path));
tokio::fs::write(&hosts_path, hosts_content)
.await
.map_err(|e| format!("/etc/hosts yazılamadı: {}", e))?;
Ok(())
}
}
/// /etc/locale.conf ve /etc/locale.gen'i yazar, sonra locale-gen çalıştırır.
pub struct ConfigureLocaleJob {
pub mount: String,
pub locale: String, // ör. "tr_TR.UTF-8"
}
#[async_trait::async_trait]
impl Job for ConfigureLocaleJob {
fn name(&self) -> &str { "job_locale" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
// Locale değeri doğrulama: yalnızca alfanumerik, '.', '_', '-', '@'
// karakterlerine izin ver. Yol manipülasyonu ve injection önlenir.
let locale_valid = !self.locale.is_empty()
&& !self.locale.contains("..")
&& !self.locale.contains('/')
&& !self.locale.contains('\n')
&& self.locale.chars().all(|c| c.is_alphanumeric() || "._-@".contains(c));
if !locale_valid {
return Err(format!("Geçersiz locale değeri: '{}'", self.locale));
}
let etc_dir = format!("{}/etc", self.mount);
let _ = tokio::fs::create_dir_all(&etc_dir).await;
let _ = tokio::fs::create_dir_all(format!("{}/etc/env.d", self.mount)).await;
// /etc/locale.conf (systemd-style)
let conf_path = format!("{}/etc/locale.conf", self.mount);
let conf = format!(
"LANG={locale}\nLC_ALL={locale}\n",
locale = self.locale
);
ui.log(format!("Yazılıyor: {}", conf_path));
let _ = tokio::fs::write(&conf_path, conf).await;
// /etc/env.d/03locale (PisiLinux native)
let pisi_locale_path = format!("{}/etc/env.d/03locale", self.mount);
let pisi_locale = format!(
"LANG={locale}\nLC_ALL={locale}\n",
locale = self.locale
);
ui.log(format!("Yazılıyor: {}", pisi_locale_path));
let _ = tokio::fs::write(&pisi_locale_path, pisi_locale).await;
// /etc/locale.gen — satırın başındaki # kaldırılır
let gen_path = format!("{}/etc/locale-gen", self.mount);
ui.log(format!("Güncelleniyor: {}", gen_path));
let content = tokio::fs::read_to_string(&gen_path).await.unwrap_or_default();
let updated = content
.lines()
.map(|line| {
let stripped = line.trim_start_matches('#').trim_start();
if stripped.starts_with(&self.locale) {
stripped.to_string() // yorumu kaldır
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n");
tokio::fs::write(&gen_path, updated + "\n")
.await
.map_err(|e| format!("/etc/locale-gen yazılamadı: {}", e))?;
// locale-gen çalıştır — kurulum sonrası locale desteği için zorunlu
ui.log("locale-gen çalıştırılıyor…");
run_chroot(&self.mount, &["locale-gen"]).await?;
Ok(())
}
}
/// Zaman dilimini ayarlar ve hwclock'u senkronize eder.
pub struct ConfigureTimezoneJob {
pub mount: String,
pub timezone: String, // ör. "Europe/Istanbul"
}
#[async_trait::async_trait]
impl Job for ConfigureTimezoneJob {
fn name(&self) -> &str { "job_timezone" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
// Path traversal koruması: timezone değeri yalnızca alfanumerik,
// '/', '_', '-', '+' karakterlerine izin verir; ".." asla geçemez.
let tz = &self.timezone;
let tz_valid = !tz.is_empty()
&& !tz.contains("..")
&& tz.chars().all(|c| c.is_alphanumeric() || "/._-+".contains(c));
if !tz_valid {
return Err(format!("Geçersiz timezone değeri: '{}'", tz));
}
// Sembolik link: /etc/localtime → /usr/share/zoneinfo/<tz>
let link_target = format!("/usr/share/zoneinfo/{}", self.timezone);
let link_path = format!("{}/etc/localtime", self.mount);
ui.log(format!("ln -sf {} {}", link_target, link_path));
// Önce varsa eski linki sil
let _ = tokio::fs::remove_file(&link_path).await;
// tokio'da Unix symlink için spawn_blocking kullan
let lt = link_target.clone();
let lp = link_path.clone();
tokio::task::spawn_blocking(move || {
std::os::unix::fs::symlink(&lt, &lp)
}).await
.map_err(|e| format!("symlink spawn hatası: {}", e))?
.map_err(|e| format!("localtime symlink: {}", e))?;
// hwclock --systohc
ui.log("hwclock --systohc çalıştırılıyor…");
// Hata kritik değil, uyar ve devam et
match run_chroot(&self.mount, &["hwclock", "--systohc"]).await {
Ok(_) => ui.log("hwclock senkronize edildi."),
Err(e) => ui.log(format!("⚠ hwclock uyarısı (kritik değil): {}", e)),
}
Ok(())
}
}
/// NTP ve manuel tarih/saat yapılandırması.
pub struct ConfigureDateTimeJob {
pub mount: String,
pub use_ntp: bool,
pub year: i32,
pub month: u32,
pub day: u32,
pub hour: u32,
pub minute: u32,
}
#[async_trait::async_trait]
impl Job for ConfigureDateTimeJob {
fn name(&self) -> &str { "job_datetime" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
if self.use_ntp {
ui.log("NTP etkinleştiriliyor…");
// systemd-timesyncd veya ntpd'yi etkinleştir
let _ = run_chroot(&self.mount, &["systemctl", "enable", "systemd-timesyncd"]).await;
let _ = run_chroot(&self.mount, &["systemctl", "start", "systemd-timesyncd"]).await;
} else {
ui.log("Manuel tarih/saat ayarlanıyor…");
let date_str = format!(
"{:04}-{:02}-{:02} {:02}:{:02}:00",
self.year, self.month, self.day, self.hour, self.minute
);
let _ = run_chroot(&self.mount, &["date", "-s", &date_str]).await;
let _ = run_chroot(&self.mount, &["hwclock", "--systohc"]).await;
}
Ok(())
}
}
/// Klavye düzenini /etc/vconsole.conf ve X11 yapılandırmasına yazar.
pub struct ConfigureKeyboardJob {
pub mount: String,
pub layout: String, // ör. "tr"
pub variant: String, // ör. "f" veya ""
}
#[async_trait::async_trait]
impl Job for ConfigureKeyboardJob {
fn name(&self) -> &str { "job_keyboard" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
// /etc/vconsole.conf (systemd-style TTY)
let vconsole_path = format!("{}/etc/vconsole.conf", self.mount);
let keymap = if self.variant.is_empty() {
self.layout.clone()
} else {
format!("{}-{}", self.layout, self.variant)
};
let vconsole = format!("KEYMAP={}\nFONT=ter-v16n\n", keymap);
ui.log(format!("Yazılıyor: {}", vconsole_path));
let _ = tokio::fs::write(&vconsole_path, vconsole).await;
// /etc/conf.d/mudur (PisiLinux native init)
let mudur_path = format!("{}/etc/conf.d/mudur", self.mount);
let mudur_conf = format!("keymap={}\nfont=ter-v16n\n", keymap);
ui.log(format!("Yazılıyor: {}", mudur_path));
let _ = tokio::fs::write(&mudur_path, mudur_conf).await;
// /etc/X11/xorg.conf.d/00-keyboard.conf (X11 için)
let x11_dir = format!("{}/etc/X11/xorg.conf.d", self.mount);
let x11_path = format!("{}/00-keyboard.conf", x11_dir);
tokio::fs::create_dir_all(&x11_dir).await
.map_err(|e| format!("{} oluşturulamadı: {}", x11_dir, e))?;
let options_line = if self.variant.is_empty() {
String::new()
} else {
format!("\n\t\tOption \"XkbVariant\" \"{}\"", self.variant)
};
let x11_conf = format!(
"Section \"InputClass\"\n\
\tIdentifier \"system-keyboard\"\n\
\tMatchIsKeyboard \"on\"\n\
\tOption \"XkbLayout\" \"{layout}\"{options}\n\
EndSection\n",
layout = self.layout,
options = options_line,
);
ui.log(format!("Yazılıyor: {}", x11_path));
tokio::fs::write(&x11_path, x11_conf)
.await
.map_err(|e| format!("xorg keyboard conf yazılamadı: {}", e))?;
Ok(())
}
}
/// Kullanıcı hesabı oluşturur ve şifresini ayarlar.
pub struct CreateUserJob {
pub mount: String,
pub username: String,
pub password: String,
pub root_password: String,
}
#[async_trait::async_trait]
impl Job for CreateUserJob {
fn name(&self) -> &str { "job_user" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
// wheel grubunun varlığını garantile (PisiLinux'ta olmayabilir)
let group_file = format!("{}/etc/group", self.mount);
let group_content = tokio::fs::read_to_string(&group_file)
.await.unwrap_or_default();
if !group_content.lines().any(|l| l.starts_with("wheel:")) {
ui.log("wheel grubu oluşturuluyor…");
run_chroot(&self.mount, &["groupadd", "-r", "wheel"]).await
.unwrap_or_default(); // hata olsa bile devam et
}
// useradd: kullanıcıyı temel gruplarla oluştur
// PisiLinux'ta "storage" grubu yoksa useradd hata verir; -M flag ile grub yoksa atla
// Güvenli yöntem: önce her grubu tek tek ekle
let groups = ["users", "pnp", "disk", "audio", "video", "power", "dialout", "lp", "lpadmin", "cdrom", "floppy"];
for grp in &groups {
if group_content.lines().any(|l| l.starts_with(&format!("{}:", grp))) {
// grup zaten var
} else {
// grup yoksa oluşturma (hata görmezden gel)
let _ = run_chroot(&self.mount, &["groupadd", grp]).await;
}
}
// Kullanıcıyı oluştur
let groups_str = groups.join(",");
ui.log(format!("useradd -m -G {} -s /bin/bash {}", groups_str, self.username));
run_chroot(&self.mount, &[
"useradd",
"-m",
"-G", &groups_str,
"-s", "/bin/bash",
&self.username,
]).await?;
// // chpasswd ile şifre ayarla (stdin üzerinden güvenli)
// ui.log(format!("Şifre ayarlanıyor: {}", self.username));
// let chpasswd_input = format!("{}:{}", self.username, self.password);
// set_password_via_chpasswd(&self.mount, &chpasswd_input).await?;
// // 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));
// sh -c kullanarak echo ile chpasswd'e şifreyi güvenli bir şekilde üflüyoruz
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
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_cmd = format!("echo 'root:{}' | chpasswd -c SHA512", self.root_password);
run_chroot(&self.mount, &["sh", "-c", &root_cmd]).await?;
}
// sudoers: wheel grubu sudo yetkisi alsın
let sudoers_dir = format!("{}/etc/sudoers.d", self.mount);
tokio::fs::create_dir_all(&sudoers_dir).await
.map_err(|e| format!("{} oluşturulamadı: {}", sudoers_dir, e))?;
let sudoers_path = format!("{}/wheel", sudoers_dir);
ui.log(format!("Yazılıyor: {}", sudoers_path));
tokio::fs::write(&sudoers_path, "%wheel ALL=(ALL:ALL) ALL\n")
.await
.map_err(|e| format!("sudoers yazılamadı: {}", e))?;
// Dosya izinleri: sudoers 440 olmalı
run_cmd("chmod", &["440", &sudoers_path]).await?;
Ok(())
}
}
// /// `chpasswd` komutuna stdin üzerinden kullanıcı:şifre gönderir.
// async fn set_password_via_chpasswd(mount: &str, input: &str) -> Result<(), String> {
// if 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::process::Command;
// let mut child = Command::new("chroot")
// .args([mount, "chpasswd"])
// .stdin(std::process::Stdio::piped())
// .stdout(std::process::Stdio::null())
// .stderr(std::process::Stdio::piped())
// .spawn()
// .map_err(|e| format!("chpasswd başlatılamadı: {}", e))?;
// if let Some(mut stdin) = child.stdin.take() {
// let mut data = input.as_bytes().to_vec();
// if !data.ends_with(b"\n") {
// data.push(b'\n');
// }
// stdin.write_all(&data).await
// .map_err(|e| format!("chpasswd stdin yazma hatası: {}", e))?;
// }
// let output = child.wait_with_output().await
// .map_err(|e| format!("chpasswd bekleme hatası: {}", e))?;
// if output.status.success() {
// Ok(())
// } else {
// Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
// }
// }
/// fstab'ı doğrudan dosyaya yazar (genfstab yerine; ISO ortamında daha güvenilir).
pub struct GenerateFstabJob {
pub mount: String,
pub root_uuid: String, // blkid ile alınır
pub efi_uuid: Option<String>,
pub swap_uuid: String,
pub encrypt_root: bool,
}
#[async_trait::async_trait]
impl Job for GenerateFstabJob {
fn name(&self) -> &str { "job_fstab" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
let etc_dir = format!("{}/etc", self.mount);
let _ = tokio::fs::create_dir_all(&etc_dir).await;
let fstab_path = format!("{}/etc/fstab", self.mount);
ui.log(format!("Yazılıyor: {}", fstab_path));
let mut fstab = String::from(
"# /etc/fstab — Yali tarafından otomatik oluşturuldu\n\
# <device> <mountpoint> <fs> <options> <dump> <pass>\n\n"
);
// Kök bölümü
if self.encrypt_root {
fstab.push_str(
"/dev/mapper/cryptroot / ext4 defaults,noatime 0 1\n"
);
} else {
fstab.push_str(&format!(
"UUID={} / ext4 defaults,noatime 0 1\n",
self.root_uuid
));
}
// EFI bölümü
if let Some(ref efi) = self.efi_uuid {
fstab.push_str(&format!(
"UUID={} /boot/efi vfat umask=0077 0 2\n",
efi
));
}
// Swap
fstab.push_str(&format!(
"UUID={} none swap sw 0 0\n",
self.swap_uuid
));
tokio::fs::write(&fstab_path, fstab)
.await
.map_err(|e| format!("fstab yazılamadı: {}", e))?;
// crypttab (LUKS için)
if self.encrypt_root {
let crypttab_path = format!("{}/etc/crypttab", self.mount);
let crypttab = format!(
"cryptroot UUID={} none luks\n",
self.root_uuid
);
ui.log(format!("Yazılıyor: {}", crypttab_path));
tokio::fs::write(&crypttab_path, crypttab)
.await
.map_err(|e| format!("crypttab yazılamadı: {}", e))?;
}
Ok(())
}
}
/// Disk bölümlerinin UUID'lerini `blkid` ile okur.
pub async fn read_partition_uuids(
plan: &PartitionPlan,
) -> Result<(String, Option<String>, String), String> {
let (root_num, swap_num): (u32, u32) = if plan.efi_mb > 0 { (3, 2) } else { (2, 1) };
let root_part = part_path(&plan.disk, root_num);
let swap_part = part_path(&plan.disk, swap_num);
// Şifreli kök: blkid'den LUKS UUID'sini al (crypttab için)
let root_uuid = blkid_uuid(&root_part).await?;
let swap_uuid = blkid_uuid(&swap_part).await?;
let efi_uuid = if plan.efi_mb > 0 {
Some(blkid_uuid(&part_path(&plan.disk, 1)).await?)
} else {
None
};
Ok((root_uuid, efi_uuid, swap_uuid))
}
async fn blkid_uuid(device: &str) -> Result<String, String> {
if DEMO_MODE.load(std::sync::atomic::Ordering::Relaxed) {
tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
return Ok(format!("demo-uuid-{}", device.replace("/", "-")));
}
let output = tokio::process::Command::new("blkid")
.args(["-s", "UUID", "-o", "value", device])
.output()
.await
.map_err(|e| format!("blkid çalıştırılamadı: {}", e))?;
let uuid = String::from_utf8_lossy(&output.stdout).trim().to_string();
if uuid.is_empty() {
Err(format!("{} için UUID bulunamadı", device))
} else {
Ok(uuid)
}
}
// ─────────────────────────────────────────────
// MANUEL BÖLÜMLEME İÇİN DESTEKLEYİCİ JOB'LAR
// ─────────────────────────────────────────────
use crate::installer::{CustomPartition, FsType};
/// Manuel bölümleme ayarlarını disk üzerinde uygulayan ve biçimlendiren Job.
pub struct CustomPartitionJob {
pub disk: String,
pub custom_partitions: Vec<CustomPartition>,
}
#[async_trait::async_trait]
impl Job for CustomPartitionJob {
fn name(&self) -> &str { "job_custom_partition" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
let is_uefi = std::path::Path::new("/sys/firmware/efi").exists();
// 0. Diski hazırla: bağlamaları ayır, kernel tablosunu temizle
prepare_disk(&self.disk, ui).await;
let is_gpt = is_uefi; // Genellikle UEFI ise GPT kullanılır
// MBR'de "primary" zorunludur, GPT'de ise bu alan isim (label) alanıdır.
let part_type_or_name = if is_gpt { "" } else { "primary" };
// 1. Bölüm tablosu oluştur
let table_label = if is_uefi { "gpt" } else { "msdos" };
ui.log(format!("parted {} mktable {}", self.disk, table_label));
run_parted(&["-s", &self.disk, "mktable", table_label]).await?;
let mut next_mb: u64 = 1; // İlk MB boot sektörü
// 2. Bölümleri parted ile oluştur
for (i, part) in self.custom_partitions.iter().enumerate() {
let part_num = (i + 1) as u32;
let end_str = if part.size_mb == 0 {
"100%".to_string()
} else {
format!("{}MB", next_mb + part.size_mb)
};
let start_str = format!("{}MB", next_mb);
let fs_label = match part.fstype {
FsType::Fat32 => "fat32",
FsType::Swap => "linux-swap",
FsType::Ext4 => "ext4",
FsType::Ext3 => "ext3",
FsType::Btrfs => "btrfs",
FsType::Xfs => "xfs",
FsType::Ntfs => "ntfs",
// LVM PV için parted "ext4" ile işaretle, pvcreate sonra
FsType::Lvm => "ext4",
// LVM LV doğrudan parted ile oluşturulmaz
FsType::LvmLv => continue,
};
ui.log(format!(
"Bölüm {} oluşturuluyor: {} - {} ({})",
part_num, start_str, end_str, fs_label
));
run_parted(&[
"-s", &self.disk, "mkpart", part_type_or_name, fs_label,
&start_str, &end_str
]).await?;
if part.mountpoint == "/boot/efi" || part.fstype == FsType::Fat32 {
run_parted(&["-s", &self.disk, "set", &part_num.to_string(), "esp", "on"]).await?;
}
if part.size_mb > 0 {
next_mb += part.size_mb;
}
}
// 2.5. Tüm mkpart bitti; kernel'e bildir ve aygıt düğümlerini bekle
sync_partition_table(&self.disk).await;
// 3. Biçimlendir (LUKS ile şifreleme desteklenir)
for part in &self.custom_partitions {
let path = &part.device;
// LUKS şifreleme (kök veya isteğe bağlı bölümler)
// Şifre, shell injection'ı önlemek için doğrudan stdin pipe üzerinden
// iletilir; `sh -c` + string interpolasyon kullanılmaz.
let fs_device = if part.encrypt && !part.luks_password.is_empty() {
let luks_name = if part.luks_name.is_empty() {
format!("crypt_{}", part.mountpoint.replace("/", "_"))
} else {
part.luks_name.clone()
};
let luks_name = luks_name.trim_start_matches('_').to_string();
ui.log(format!("cryptsetup luksFormat {}", path));
run_luks_format(path, &part.luks_password).await
.map_err(|e| format!("LUKS format hatası ({}): {}", path, e))?;
ui.log(format!("cryptsetup open {} {}", path, luks_name));
run_luks_open(path, &luks_name, &part.luks_password).await
.map_err(|e| format!("LUKS open hatası ({}): {}", path, e))?;
format!("/dev/mapper/{}", luks_name)
} else {
path.clone()
};
match part.fstype {
FsType::Fat32 => {
ui.log(format!("mkfs.fat -F32 {}", fs_device));
run_cmd("mkfs.fat", &["-F32", &fs_device]).await?;
}
FsType::Swap => {
ui.log(format!("mkswap {}", path));
run_cmd("mkswap", &[path]).await?;
ui.log(format!("swapon {}", path));
run_cmd("swapon", &[path]).await?;
}
FsType::Ext4 => {
ui.log(format!("mkfs.ext4 -F {}", fs_device));
run_cmd("mkfs.ext4", &["-F", &fs_device]).await?;
}
FsType::Ext3 => {
ui.log(format!("mkfs.ext3 -F {}", fs_device));
run_cmd("mkfs.ext3", &["-F", &fs_device]).await?;
}
FsType::Btrfs => {
ui.log(format!("mkfs.btrfs -f {}", fs_device));
run_cmd("mkfs.btrfs", &["-f", &fs_device]).await?;
}
FsType::Xfs => {
ui.log(format!("mkfs.xfs -f {}", fs_device));
run_cmd("mkfs.xfs", &["-f", &fs_device]).await?;
}
FsType::Ntfs => {
ui.log(format!("mkfs.ntfs -f {}", fs_device));
run_cmd("mkfs.ntfs", &["-f", &fs_device]).await?;
}
FsType::Lvm => {
ui.log(format!("pvcreate {}", path));
run_cmd("pvcreate", &[path]).await?;
}
// LV ayrı bir job'da oluşturulur
FsType::LvmLv => {}
}
}
Ok(())
}
}
/// LVM Volume Group ve Logical Volume'leri oluşturur.
pub struct SetupLvmJob {
pub volume_groups: Vec<crate::installer::VolumeGroup>,
pub logical_volumes: Vec<crate::installer::LogicalVolume>,
}
#[async_trait::async_trait]
impl Job for SetupLvmJob {
fn name(&self) -> &str { "job_setup_lvm" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
for vg in &self.volume_groups {
let pv_str = vg.pv_devices.join(" ");
ui.log(format!("vgcreate {} {}", vg.name, pv_str));
run_cmd("vgcreate", &[&vg.name, &pv_str]).await?;
}
for lv in &self.logical_volumes {
let size_str = if lv.size_mb == 0 {
"100%FREE".to_string()
} else {
format!("{}M", lv.size_mb)
};
let lv_path = format!("/dev/{}/{}", lv.vg_name, lv.name);
ui.log(format!("lvcreate -L {} -n {} {}", size_str, lv.name, lv.vg_name));
run_cmd("lvcreate", &["-L", &size_str, "-n", &lv.name, &lv.vg_name]).await?;
let fs_cmd = match lv.fstype {
crate::installer::FsType::Ext4 => "mkfs.ext4",
crate::installer::FsType::Ext3 => "mkfs.ext3",
crate::installer::FsType::Btrfs => "mkfs.btrfs",
crate::installer::FsType::Xfs => "mkfs.xfs",
_ => continue,
};
ui.log(format!("{} {}", fs_cmd, lv_path));
run_cmd(fs_cmd, &[&lv_path]).await?;
}
Ok(())
}
}
/// Manuel bölümleri ve mantıksal hacimleri kök dizinden başlayarak hiyerarşik olarak hedef yola bağlayan Job.
pub struct MountCustomPartitionsJob {
pub custom_partitions: Vec<CustomPartition>,
pub logical_volumes: Vec<crate::installer::LogicalVolume>,
pub mount: String,
}
impl MountCustomPartitionsJob {
/// Şifreli bölümler için mapper yolunu döndürür.
fn device_path(&self, part: &CustomPartition) -> String {
if part.encrypt {
let luks_name = if part.luks_name.is_empty() {
format!("crypt_{}", part.mountpoint.replace("/", "_").trim_start_matches('_'))
} else {
part.luks_name.clone()
};
format!("/dev/mapper/{}", luks_name)
} else {
part.device.clone()
}
}
}
#[async_trait::async_trait]
impl Job for MountCustomPartitionsJob {
fn name(&self) -> &str { "job_mount_custom" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
// 1. Kök (/) bölümünü (fiziksel partition veya LVM LV) bul ve bağla
let root_device = if let Some(root_part) = self.custom_partitions.iter().find(|p| p.mountpoint == "/") {
self.device_path(root_part)
} else if let Some(root_lv) = self.logical_volumes.iter().find(|lv| lv.mountpoint == "/") {
format!("/dev/{}/{}", root_lv.vg_name, root_lv.name)
} else {
return Err("Kök (/) bölümü manuel listede veya mantıksal bölümlerde bulunamadı.".to_string());
};
ui.log(format!("Kök dizin bağlanıyor: {}{}", root_device, self.mount));
run_cmd("mount", &[&root_device, &self.mount]).await?;
// 2. Diğer bölümleri ve mantıksal hacimleri topla, sırala ve bağla (derinliğe göre artan sırada)
struct MountItem {
device: String,
mountpoint: String,
}
let mut mount_items = Vec::new();
for part in &self.custom_partitions {
if part.mountpoint != "/" && part.fstype != FsType::Swap && !part.mountpoint.is_empty() {
mount_items.push(MountItem {
device: self.device_path(part),
mountpoint: part.mountpoint.clone(),
});
}
}
for lv in &self.logical_volumes {
if lv.mountpoint != "/" && lv.fstype != FsType::Swap && !lv.mountpoint.is_empty() {
mount_items.push(MountItem {
device: format!("/dev/{}/{}", lv.vg_name, lv.name),
mountpoint: lv.mountpoint.clone(),
});
}
}
mount_items.sort_by_key(|item| item.mountpoint.len());
for item in mount_items {
let target_path = format!("{}{}", self.mount, item.mountpoint);
ui.log(format!("Bağlama noktası dizini oluşturuluyor: {}", target_path));
tokio::fs::create_dir_all(&target_path).await
.map_err(|e| format!("{} oluşturulamadı: {}", target_path, e))?;
ui.log(format!("Bağlanıyor: {}{}", item.device, target_path));
run_cmd("mount", &[&item.device, &target_path]).await?;
}
Ok(())
}
}
/// Manuel bölümleme planına göre fstab dosyası üreten Job.
pub struct FstabFromCustomPartitionsJob {
pub custom_partitions: Vec<CustomPartition>,
pub logical_volumes: Vec<crate::installer::LogicalVolume>,
pub mount: String,
}
#[async_trait::async_trait]
impl Job for FstabFromCustomPartitionsJob {
fn name(&self) -> &str { "job_fstab" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
let etc_dir = format!("{}/etc", self.mount);
let _ = tokio::fs::create_dir_all(&etc_dir).await;
let fstab_path = format!("{}/etc/fstab", self.mount);
ui.log(format!("Yazılıyor: {}", fstab_path));
let mut fstab = String::from(
"# /etc/fstab — Yali tarafından otomatik (manuel mod) oluşturuldu\n\
# <device> <mountpoint> <fs> <options> <dump> <pass>\n\n"
);
let mut crypttab = String::from(
"# /etc/crypttab — Yali tarafından otomatik oluşturuldu\n"
);
for part in &self.custom_partitions {
if part.mountpoint.is_empty() { continue; }
let is_encrypted = part.encrypt && !part.luks_password.is_empty();
let device_field = if is_encrypted {
let luks_uuid = blkid_uuid(&part.device).await?;
let luks_name = if part.luks_name.is_empty() {
format!("crypt_{}", part.mountpoint.replace("/", "_").trim_start_matches('_'))
} else {
part.luks_name.clone()
};
crypttab.push_str(&format!(
"{} UUID={} none luks\n",
luks_name, luks_uuid
));
format!("/dev/mapper/{:<30}", luks_name)
} else {
ui.log(format!("{} için UUID okunuyor…", part.device));
let uuid = blkid_uuid(&part.device).await?;
format!("UUID={:<36}", uuid)
};
let (mp, options, dump, pass) = match part.fstype {
FsType::Swap => ("none", "sw", "0", "0"),
FsType::Fat32 => ("/boot/efi", "umask=0077", "0", "2"),
FsType::Ext4 | FsType::Ext3 if part.mountpoint == "/" => ("/", "defaults,noatime", "0", "1"),
_ => {
let pass_val = if part.mountpoint == "/" { "1" } else { "2" };
(part.mountpoint.as_str(), "defaults,noatime", "0", pass_val)
}
};
let fs_name = match part.fstype {
FsType::Ext4 => "ext4",
FsType::Ext3 => "ext3",
FsType::Btrfs => "btrfs",
FsType::Xfs => "xfs",
FsType::Fat32 => "vfat",
FsType::Ntfs => "ntfs-3g",
FsType::Swap => "swap",
FsType::Lvm | FsType::LvmLv => "ext4",
};
fstab.push_str(&format!(
"{} {:<12} {:<6} {:<18} {} {}\n",
device_field, mp, fs_name, options, dump, pass
));
}
// LVM mantıksal bölümlerini fstab'a ekle
for lv in &self.logical_volumes {
if lv.mountpoint.is_empty() { continue; }
let device_field = format!("/dev/{}/{}", lv.vg_name, lv.name);
let (mp, options, dump, pass) = match lv.fstype {
FsType::Swap => ("none", "sw", "0", "0"),
FsType::Fat32 => ("/boot/efi", "umask=0077", "0", "2"),
FsType::Ext4 | FsType::Ext3 if lv.mountpoint == "/" => ("/", "defaults,noatime", "0", "1"),
_ => {
let pass_val = if lv.mountpoint == "/" { "1" } else { "2" };
(lv.mountpoint.as_str(), "defaults,noatime", "0", pass_val)
}
};
let fs_name = match lv.fstype {
FsType::Ext4 => "ext4",
FsType::Ext3 => "ext3",
FsType::Btrfs => "btrfs",
FsType::Xfs => "xfs",
FsType::Fat32 => "vfat",
FsType::Ntfs => "ntfs-3g",
FsType::Swap => "swap",
_ => "ext4",
};
fstab.push_str(&format!(
"{:<42} {:<12} {:<6} {:<18} {} {}\n",
device_field, mp, fs_name, options, dump, pass
));
}
tokio::fs::write(&fstab_path, fstab)
.await
.map_err(|e| format!("fstab yazılamadı: {}", e))?;
// crypttab (sadece şifreli bölümler için)
let crypttab_device_count = crypttab.lines().count().saturating_sub(1); // header satırını sayma
if crypttab_device_count > 0 {
let crypttab_path = format!("{}/etc/crypttab", self.mount);
ui.log(format!("Yazılıyor: {}", crypttab_path));
let _ = tokio::fs::write(&crypttab_path, crypttab).await;
}
Ok(())
}
}
/// Manuel bölümleme kullanıldığında derlenecek kurulum iş kuyruğu.
pub fn build_custom_job_queue(
disk: String,
custom_partitions: Vec<CustomPartition>,
volume_groups: Vec<crate::installer::VolumeGroup>,
logical_volumes: Vec<crate::installer::LogicalVolume>,
cfg: JobQueueConfig,
) -> JobQueue {
DEMO_MODE.store(cfg.demo_mode, std::sync::atomic::Ordering::Relaxed);
let is_uefi = std::path::Path::new("/sys/firmware/efi").exists();
let encrypt_root = custom_partitions.iter().any(|p| p.mountpoint == "/" && p.encrypt);
let mt = cfg.mount.clone();
let src = if cfg.source.is_empty() { detect_source_dir() } else { cfg.source };
let mut jobs: Vec<Box<dyn Job>> = vec![
Box::new(CustomPartitionJob { disk: disk.clone(), custom_partitions: custom_partitions.clone() }),
Box::new(SetupLvmJob { volume_groups: volume_groups.clone(), logical_volumes: logical_volumes.clone() }),
Box::new(MountCustomPartitionsJob { custom_partitions: custom_partitions.clone(), logical_volumes: logical_volumes.clone(), mount: mt.clone() }),
Box::new(MountBindJob { mount: mt.clone() }),
Box::new(CopyFilesJob { source: src, target: mt.clone() }),
Box::new(FstabFromCustomPartitionsJob { custom_partitions: custom_partitions.clone(), logical_volumes: logical_volumes.clone(), mount: mt.clone() }),
Box::new(ConfigureHostnameJob { mount: mt.clone(), hostname: cfg.hostname.clone() }),
Box::new(ConfigureLocaleJob { mount: mt.clone(), locale: cfg.locale.clone() }),
Box::new(ConfigureTimezoneJob { mount: mt.clone(), timezone: cfg.timezone.clone() }),
Box::new(ConfigureDateTimeJob {
mount: mt.clone(),
use_ntp: cfg.use_ntp,
year: cfg.manual_year,
month: cfg.manual_month,
day: cfg.manual_day,
hour: cfg.manual_hour,
minute: cfg.manual_minute,
}),
Box::new(ConfigureKeyboardJob { mount: mt.clone(), layout: cfg.kb_layout.clone(), variant: cfg.kb_variant.clone() }),
Box::new(CreateUserJob { mount: mt.clone(), username: cfg.username.clone(), password: cfg.password.clone(), root_password: cfg.root_password.clone() }),
Box::new(GenerateInitramfsJob { mount: mt.clone(), encrypt_root, lvm_active: !volume_groups.is_empty() }),
];
jobs.push(Box::new(InstallGrubJob {
disk: disk.clone(),
mount: mt.clone(),
is_uefi,
boot_device: cfg.boot_device.clone(),
timeout: cfg.bootloader_timeout,
password: cfg.bootloader_password.clone(),
kernel_options: cfg.kernel_options.clone(),
encrypt_root,
}));
jobs.push(Box::new(ConfigureDisplayManagerJob {
mount: mt.clone(),
display_manager: cfg.display_manager.clone(),
autologin: cfg.autologin,
username: cfg.username.clone(),
desktop_environment: cfg.desktop_environment.clone(),
}));
jobs.push(Box::new(RunComarJob { mount: mt.clone() }));
jobs.push(Box::new(RunLdconfigJob { mount: mt.clone() }));
jobs.push(Box::new(UpdateEnvironmentJob { mount: mt.clone() }));
if !cfg.selected_package_groups.is_empty() {
jobs.push(Box::new(InstallExtraPackagesJob {
mount: mt.clone(),
package_groups: cfg.selected_package_groups.clone(),
}));
}
jobs.push(Box::new(CleanupLiveJob { mount: mt.clone() }));
jobs.push(Box::new(UnmountBindJob { mount: mt.clone() }));
JobQueue::new(jobs)
}