Programação de Sistemas — Arquivos, Processos e Sinais

Programação de Sistemas — Arquivos, Processos e Sinais

Descer ao nível do sistema operacional traz de volta arquivos, processos e sinais — agora com Result em vez de código de erro. O texto trata da leitura em blocos que não estoura a memória, do processo filho que precisa ser esperado, e do tratador de sinal que só pode registrar o que aconteceu.
Rust

• • 19 min de leitura

 

Rust nasceu como linguagem de sistemas. Não é por acaso que ela substituiu C++ em partes críticas do kernel Linux, no Firefox, e em sistemas embarcados de segurança crítica. Neste artigo descemos ao nível do sistema operacional: manipulação de arquivos com controle total, criação e gerenciamento de processos filhos, e tratamento de sinais Unix.

Este é território onde erros custam caro — corrupção de dados, processos zumbis, comportamento indefinido. Rust oferece as mesmas primitivas de baixo nível que C, mas com as garantias que tornaram a linguagem famosa.

Arquivos — além do básico

Vimos std::fs::read_to_string e std::fs::write nos artigos anteriores (Autenticação JWT — Protegendo sua API com Tokens, Testes Avançados — Propriedades, Mocks e Benchmarks, WebAssembly com Rust — Executando Rust no Navegador). Mas para controle real sobre arquivos, precisamos de std::fs::File e std::io:

use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write};

fn demonstrar_operacoes_arquivo() -> io::Result<()> {
    // Criar arquivo para escrita
    let mut arquivo = File::create("dados.txt")?;
    arquivo.write_all(b"Linha 1
Linha 2
Linha 3
")?;

    // Abrir para leitura
    let arquivo = File::open("dados.txt")?;
    let leitor = BufReader::new(arquivo);

    println!("Conteúdo linha a linha:");
    for (i, linha) in leitor.lines().enumerate() {
        println!("  {}: {}", i + 1, linha?);
    }

    // Abrir com controle total sobre flags
    let mut arquivo = OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)      // criar se não existir
        .append(false)     // não concatenar — sobrescrever
        .open("log.txt")?;

    writeln!(arquivo, "Entrada de log: {}", chrono_simples())?;

    // Seek — posicionar cursor no arquivo
    arquivo.seek(SeekFrom::Start(0))?;
    let mut conteudo = String::new();
    arquivo.read_to_string(&mut conteudo)?;
    println!("Log completo:
{conteudo}");

    // Seek do final do arquivo
    let tamanho = arquivo.seek(SeekFrom::End(0))?;
    println!("Tamanho: {tamanho} bytes");

    Ok(())
}

fn chrono_simples() -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let secs = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs();
    format!("Unix timestamp: {secs}")
}

fn main() -> io::Result<()> {
    demonstrar_operacoes_arquivo()
}

Escrita eficiente com BufWriter

Escrever byte a byte em um arquivo é extremamente lento — cada write pode resultar em uma syscall. BufWriter acumula dados em memória e faz escritas em lotes:

use std::fs::File;
use std::io::{BufWriter, Write};
use std::time::Instant;

fn escrever_sem_buffer(n: usize) -> std::io::Result<()> {
    let mut f = File::create("sem_buffer.txt")?;
    for i in 0..n {
        writeln!(f, "linha {i}")?;
    }
    Ok(())
}

fn escrever_com_buffer(n: usize) -> std::io::Result<()> {
    let f = File::create("com_buffer.txt")?;
    let mut f = BufWriter::new(f);
    for i in 0..n {
        writeln!(f, "linha {i}")?;
    }
    f.flush()?; // garante que o buffer seja escrito
    Ok(())
}

fn main() -> std::io::Result<()> {
    let n = 100_000;

    let inicio = Instant::now();
    escrever_sem_buffer(n)?;
    println!("Sem buffer: {:?}", inicio.elapsed());

    let inicio = Instant::now();
    escrever_com_buffer(n)?;
    println!("Com buffer: {:?}", inicio.elapsed());

    // Saída típica:
    // Sem buffer: 1.2s
    // Com buffer: 45ms

    Ok(())
}

Leitura de arquivos grandes linha a linha

Para arquivos que não cabem na memória, processe linha a linha:

use std::fs::File;
use std::io::{BufRead, BufReader};

fn processar_csv_grande(caminho: &str) -> std::io::Result<EstatisticasCSV> {
    let arquivo = File::open(caminho)?;
    let leitor = BufReader::new(arquivo);
    let mut linhas = leitor.lines();

    // Lê cabeçalho
    let cabecalho = linhas.next()
        .ok_or(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "Arquivo vazio"
        ))??;

    let colunas: Vec<&str> = cabecalho.split(',').collect();
    println!("Colunas: {:?}", colunas);

    let mut total_linhas = 0usize;
    let mut total_erros = 0usize;
    let mut soma_valores = 0.0f64;

    for resultado in linhas {
        let linha = resultado?;
        total_linhas += 1;

        let campos: Vec<&str> = linha.split(',').collect();
        if campos.len() < 2 {
            total_erros += 1;
            continue;
        }

        if let Ok(valor) = campos[1].trim().parse::<f64>() {
            soma_valores += valor;
        } else {
            total_erros += 1;
        }
    }

    Ok(EstatisticasCSV {
        total_linhas,
        total_erros,
        media: if total_linhas > 0 {
            soma_valores / total_linhas as f64
        } else {
            0.0
        },
    })
}

struct EstatisticasCSV {
    total_linhas: usize,
    total_erros: usize,
    media: f64,
}

fn main() -> std::io::Result<()> {
    // Cria arquivo de teste
    {
        use std::io::Write;
        let mut f = std::fs::File::create("dados.csv")?;
        writeln!(f, "nome,valor")?;
        writeln!(f, "alpha,10.5")?;
        writeln!(f, "beta,20.3")?;
        writeln!(f, "gamma,invalido")?;
        writeln!(f, "delta,15.8")?;
    }

    match processar_csv_grande("dados.csv") {
        Ok(stats) => {
            println!("Linhas processadas : {}", stats.total_linhas);
            println!("Erros encontrados  : {}", stats.total_erros);
            println!("Média dos valores  : {:.2}", stats.media);
        }
        Err(e) => eprintln!("Erro: {e}"),
    }

    Ok(())
}

Metadados e sistema de arquivos

use std::fs;
use std::path::Path;

fn inspecionar_caminho(caminho: &str) -> std::io::Result<()> {
    let path = Path::new(caminho);

    if !path.exists() {
        println!("{caminho}: não existe");
        return Ok(());
    }

    let metadata = fs::metadata(path)?;

    println!("── {caminho} ──");
    println!("  É arquivo    : {}", metadata.is_file());
    println!("  É diretório  : {}", metadata.is_dir());
    println!("  Tamanho      : {} bytes", metadata.len());
    println!("  Somente leit.: {}", metadata.permissions().readonly());

    if let Ok(modificado) = metadata.modified() {
        use std::time::SystemTime;
        let duracao = SystemTime::now()
            .duration_since(modificado)
            .unwrap_or_default();
        println!("  Modificado há: {}s", duracao.as_secs());
    }

    Ok(())
}

fn listar_diretorio_recursivo(dir: &Path, prefixo: &str) -> std::io::Result<()> {
    if !dir.is_dir() {
        return Ok(());
    }

    let mut entradas: Vec<_> = fs::read_dir(dir)?
        .filter_map(|e| e.ok())
        .collect();

    // Ordena: diretórios primeiro, depois arquivos, ambos por nome
    entradas.sort_by(|a, b| {
        let a_dir = a.path().is_dir();
        let b_dir = b.path().is_dir();
        match (a_dir, b_dir) {
            (true, false) => std::cmp::Ordering::Less,
            (false, true) => std::cmp::Ordering::Greater,
            _ => a.file_name().cmp(&b.file_name()),
        }
    });

    for (i, entrada) in entradas.iter().enumerate() {
        let e_ultimo = i == entradas.len() - 1;
        let conector = if e_ultimo { "└── " } else { "├── " };
        let extensao = if e_ultimo { "    " } else { "│   " };

        let nome = entrada.file_name();
        let nome = nome.to_string_lossy();
        let path = entrada.path();

        if path.is_dir() {
            println!("{prefixo}{conector}{nome}/");
            let novo_prefixo = format!("{prefixo}{extensao}");
            listar_diretorio_recursivo(&path, &novo_prefixo)?;
        } else {
            let meta = fs::metadata(&path)?;
            println!("{prefixo}{conector}{nome} ({} bytes)", meta.len());
        }
    }

    Ok(())
}

fn main() -> std::io::Result<()> {
    // Cria estrutura de diretórios para demonstração
    fs::create_dir_all("projeto/src")?;
    fs::create_dir_all("projeto/tests")?;
    fs::write("projeto/Cargo.toml", "[package]
name = "demo"")?;
    fs::write("projeto/src/main.rs", "fn main() {}")?;
    fs::write("projeto/src/lib.rs", "pub mod utils;")?;
    fs::write("projeto/tests/integracao.rs", "#[test] fn ok() {}")?;

    println!("projeto/");
    listar_diretorio_recursivo(Path::new("projeto"), "")?;

    inspecionar_caminho("projeto/Cargo.toml")?;

    // Limpeza
    fs::remove_dir_all("projeto")?;

    Ok(())
}

Saída:

projeto/
├── src/
│   ├── lib.rs (18 bytes)
│   └── main.rs (14 bytes)
├── tests/
│   └── integracao.rs (22 bytes)
└── Cargo.toml (27 bytes)

── projeto/Cargo.toml ──
  É arquivo    : true
  É diretório  : false
  Tamanho      : 27 bytes
  Somente leit.: false
  Modificado há: 0s

Processos — criando e gerenciando filhos

std::process::Command permite criar subprocessos com controle total sobre stdin, stdout e stderr:

use std::process::{Command, Stdio};
use std::io::Write;

fn executar_comando_simples() {
    // Executa e espera terminar
    let saida = Command::new("echo")
        .arg("Olá do processo filho!")
        .output()
        .expect("Falha ao executar echo");

    println!("Status : {}", saida.status);
    println!("Stdout : {}", String::from_utf8_lossy(&saida.stdout));
    println!("Stderr : {}", String::from_utf8_lossy(&saida.stderr));
}

fn pipeline_de_comandos() -> std::io::Result<()> {
    // Equivalente a: echo "hello world" | tr '[:lower:]' '[:upper:]'
    let echo = Command::new("echo")
        .arg("hello world from rust")
        .stdout(Stdio::piped())
        .spawn()?;

    let tr = Command::new("tr")
        .arg("[:lower:]")
        .arg("[:upper:]")
        .stdin(echo.stdout.unwrap())
        .output()?;

    println!("Pipeline: {}", String::from_utf8_lossy(&tr.stdout));
    Ok(())
}

fn enviar_para_stdin() -> std::io::Result<()> {
    // Envia dados para o stdin de um processo
    let mut filho = Command::new("cat")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()?;

    // Escreve no stdin do filho
    if let Some(ref mut stdin) = filho.stdin {
        writeln!(stdin, "Linha enviada ao stdin do processo filho")?;
        writeln!(stdin, "Segunda linha")?;
    }

    // Fecha stdin — o processo filho saberá que acabou
    drop(filho.stdin.take());

    let saida = filho.wait_with_output()?;
    println!("Cat retornou: {}", String::from_utf8_lossy(&saida.stdout));

    Ok(())
}

fn verificar_comando_disponivel(comando: &str) -> bool {
    Command::new("which")
        .arg(comando)
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

fn main() -> std::io::Result<()> {
    executar_comando_simples();
    println!();

    if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
        pipeline_de_comandos()?;
        enviar_para_stdin()?;
    }

    for cmd in &["git", "cargo", "rustc", "python3"] {
        println!("{cmd}: {}", if verificar_comando_disponivel(cmd) {
            "disponível"
        } else {
            "não encontrado"
        });
    }

    Ok(())
}

Processo em background com timeout

use std::process::{Command, Stdio};
use std::time::Duration;
use std::thread;

fn executar_com_timeout(
    programa: &str,
    args: &[&str],
    timeout: Duration,
) -> Result<String, String> {
    let mut filho = Command::new(programa)
        .args(args)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .map_err(|e| format!("Falha ao iniciar processo: {e}"))?;

    // Aguarda com timeout em thread separada
    let handle = thread::spawn(move || {
        filho.wait_with_output()
    });

    match handle.join().ok()
        .and_then(|r| r.ok())
    {
        Some(saida) if saida.status.success() => {
            Ok(String::from_utf8_lossy(&saida.stdout).to_string())
        }
        Some(saida) => {
            Err(format!(
                "Processo falhou ({}): {}",
                saida.status,
                String::from_utf8_lossy(&saida.stderr)
            ))
        }
        None => Err("Timeout ou erro ao aguardar processo".to_string()),
    }
}

fn main() {
    match executar_com_timeout("ls", &["-la", "/tmp"], Duration::from_secs(5)) {
        Ok(saida) => println!("Saída:
{saida}"),
        Err(e)    => eprintln!("Erro: {e}"),
    }
}

Variáveis de ambiente e argumentos

use std::env;

fn main() {
    // Argumentos da linha de comando
    let args: Vec<String> = env::args().collect();
    println!("Programa: {}", args[0]);
    println!("Argumentos: {:?}", &args[1..]);

    // Variáveis de ambiente
    match env::var("HOME") {
        Ok(home) => println!("HOME: {home}"),
        Err(_)   => println!("HOME não definida"),
    }

    // Com valor padrão
    let porta = env::var("PORT")
        .unwrap_or_else(|_| "8080".to_string());
    println!("Porta: {porta}");

    // Listar todas as variáveis de ambiente
    println!("
Variáveis de ambiente (primeiras 5):");
    for (chave, valor) in env::vars().take(5) {
        println!("  {chave}={valor}");
    }

    // Diretório atual
    match env::current_dir() {
        Ok(dir)  => println!("
Diretório atual: {}", dir.display()),
        Err(e)   => eprintln!("Erro ao obter diretório: {e}"),
    }

    // Diretório do executável
    match env::current_exe() {
        Ok(exe)  => println!("Executável: {}", exe.display()),
        Err(e)   => eprintln!("Erro: {e}"),
    }
}

Sinais Unix com signal-hook

Sinais Unix são notificações assíncronas enviadas a processos — SIGTERM para término gracioso, SIGINT para Ctrl+C, SIGHUP para recarregar configuração. A biblioteca signal-hook oferece tratamento seguro de sinais em Rust:

[dependencies]
signal-hook = "0.3"
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
tokio = { version = "1", features = ["full"] }
use signal_hook::consts::{SIGINT, SIGTERM, SIGHUP};
use signal_hook::iterator::Signals;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;

fn servidor_com_sinais() {
    // Flag atômica para shutdown gracioso
    let rodando = Arc::new(AtomicBool::new(true));
    let rodando_clone = Arc::clone(&rodando);

    // Thread dedicada ao tratamento de sinais
    let mut sinais = Signals::new([SIGINT, SIGTERM, SIGHUP])
        .expect("Falha ao registrar sinais");

    thread::spawn(move || {
        for sinal in &mut sinais {
            match sinal {
                SIGINT | SIGTERM => {
                    println!("
[Sinal {}] Encerrando graciosamente...", sinal);
                    rodando_clone.store(false, Ordering::Relaxed);
                    break;
                }
                SIGHUP => {
                    println!("[SIGHUP] Recarregando configuração...");
                    // Em produção: recarrega config aqui
                }
                _ => unreachable!(),
            }
        }
    });

    // Loop principal do servidor
    println!("Servidor iniciado. Pressione Ctrl+C para encerrar.");
    let mut iteracao = 0u32;

    while rodando.load(Ordering::Relaxed) {
        iteracao += 1;
        println!("  Processando iteração {iteracao}...");
        thread::sleep(Duration::from_millis(500));
    }

    // Shutdown gracioso
    println!("Realizando limpeza...");
    thread::sleep(Duration::from_millis(100));
    println!("Servidor encerrado com sucesso.");
}

fn main() {
    servidor_com_sinais();
}

Shutdown gracioso com Tokio

Em aplicações assíncronas com Tokio, o padrão de shutdown gracioso é diferente:

use tokio::signal;
use tokio::sync::broadcast;
use tokio::time::{sleep, Duration};

async fn worker(
    id: u32,
    mut shutdown_rx: broadcast::Receiver<()>,
) {
    println!("Worker {id} iniciado");

    loop {
        tokio::select! {
            // Trabalho normal
            _ = sleep(Duration::from_millis(500)) => {
                println!("  Worker {id}: processando...");
            }

            // Recebeu sinal de shutdown
            _ = shutdown_rx.recv() => {
                println!("  Worker {id}: encerrando graciosamente");
                // Cleanup aqui: fechar conexões, flush buffers, etc.
                sleep(Duration::from_millis(100)).await;
                println!("  Worker {id}: encerrado");
                break;
            }
        }
    }
}

#[tokio::main]
async fn main() {
    let (shutdown_tx, _) = broadcast::channel::<()>(1);

    // Lança workers
    let mut handles = Vec::new();
    for id in 1..=3 {
        let rx = shutdown_tx.subscribe();
        handles.push(tokio::spawn(worker(id, rx)));
    }

    println!("Servidor com {} workers. Ctrl+C para encerrar.", handles.len());

    // Aguarda sinal de interrupção
    signal::ctrl_c()
        .await
        .expect("Falha ao instalar handler de Ctrl+C");

    println!("
Ctrl+C recebido — enviando shutdown para workers...");

    // Notifica todos os workers
    let _ = shutdown_tx.send(());

    // Aguarda todos os workers terminarem
    for handle in handles {
        let _ = handle.await;
    }

    println!("Todos os workers encerrados. Servidor finalizado.");
}

Um programa completo: monitor de sistema

Vamos combinar arquivos, processos e sinais em um monitor de sistema que coleta métricas e as registra em arquivo:

use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};

#[derive(Debug)]
struct MetricasSistema {
    timestamp: u64,
    processos: usize,
    carga: f64,
    uso_disco_tmp: u64,
}

fn coletar_metricas() -> MetricasSistema {
    use std::time::{SystemTime, UNIX_EPOCH};

    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs();

    // Conta processos via ps
    let processos = Command::new("sh")
        .arg("-c")
        .arg("ps aux | wc -l")
        .output()
        .map(|o| {
            String::from_utf8_lossy(&o.stdout)
                .trim()
                .parse::<usize>()
                .unwrap_or(0)
                .saturating_sub(1) // remove linha de cabeçalho
        })
        .unwrap_or(0);

    // Lê carga do sistema no Linux
    let carga = if cfg!(target_os = "linux") {
        std::fs::read_to_string("/proc/loadavg")
            .ok()
            .and_then(|s| s.split_whitespace().next().map(str::to_string))
            .and_then(|s| s.parse::<f64>().ok())
            .unwrap_or(0.0)
    } else {
        0.0
    };

    // Uso do diretório /tmp
    let uso_disco_tmp = Command::new("du")
        .args(["-sb", "/tmp"])
        .output()
        .map(|o| {
            String::from_utf8_lossy(&o.stdout)
                .split_whitespace()
                .next()
                .and_then(|s| s.parse::<u64>().ok())
                .unwrap_or(0)
        })
        .unwrap_or(0);

    MetricasSistema {
        timestamp,
        processos,
        carga,
        uso_disco_tmp,
    }
}

fn formatar_bytes(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;

    if bytes >= GB {
        format!("{:.1}GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.1}MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.1}KB", bytes as f64 / KB as f64)
    } else {
        format!("{bytes}B")
    }
}

fn iniciar_monitor(
    intervalo: Duration,
    arquivo_log: &str,
    rodando: Arc<AtomicBool>,
) -> std::io::Result<()> {
    let arquivo = OpenOptions::new()
        .create(true)
        .append(true)
        .open(arquivo_log)?;

    let mut escritor = BufWriter::new(arquivo);

    // Cabeçalho CSV
    writeln!(escritor, "timestamp,processos,carga,disco_tmp")?;
    escritor.flush()?;

    let inicio = Instant::now();
    let mut amostras = 0u32;

    println!("Monitor iniciado. Log: {arquivo_log}");
    println!("Intervalo: {}ms", intervalo.as_millis());
    println!("{:>12} {:>10} {:>8} {:>10}",
        "Timestamp", "Processos", "Carga", "Disco/tmp");
    println!("{}", "─".repeat(44));

    while rodando.load(Ordering::Relaxed) {
        let metricas = coletar_metricas();
        amostras += 1;

        // Log em arquivo
        writeln!(
            escritor,
            "{},{},{:.2},{}",
            metricas.timestamp,
            metricas.processos,
            metricas.carga,
            metricas.uso_disco_tmp,
        )?;

        // Flush periódico — a cada 10 amostras
        if amostras % 10 == 0 {
            escritor.flush()?;
        }

        // Exibição no terminal
        println!(
            "{:>12} {:>10} {:>8.2} {:>10}",
            metricas.timestamp,
            metricas.processos,
            metricas.carga,
            formatar_bytes(metricas.uso_disco_tmp),
        );

        thread::sleep(intervalo);
    }

    // Flush final garantido
    escritor.flush()?;

    println!("
── Relatório Final ──");
    println!("Amostras coletadas : {amostras}");
    println!("Tempo total        : {:.1}s", inicio.elapsed().as_secs_f64());
    println!("Log salvo em       : {arquivo_log}");

    Ok(())
}

fn main() -> std::io::Result<()> {
    let rodando = Arc::new(AtomicBool::new(true));
    let rodando_ctrlc = Arc::clone(&rodando);

    // Handler de Ctrl+C
    ctrlc::set_handler(move || {
        println!("
[Interrompido] Encerrando...");
        rodando_ctrlc.store(false, Ordering::Relaxed);
    })
    .expect("Erro ao configurar handler Ctrl+C");

    iniciar_monitor(
        Duration::from_secs(2),
        "metricas.csv",
        rodando,
    )
}

Adicione ao Cargo.toml:

ctrlc = "3"

Permissões e atributos de arquivo

use std::fs;
use std::os::unix::fs::PermissionsExt; // apenas Unix

fn gerenciar_permissoes(caminho: &str) -> std::io::Result<()> {
    // Criar arquivo de teste
    fs::write(caminho, "conteúdo secreto")?;

    // Ler permissões atuais
    let meta = fs::metadata(caminho)?;
    let perms = meta.permissions();
    println!("Permissões atuais: {:o}", perms.mode() & 0o777);

    // Tornar somente leitura
    let mut novas_perms = perms.clone();
    novas_perms.set_readonly(true);
    fs::set_permissions(caminho, novas_perms)?;

    println!("Após set_readonly:");
    let meta = fs::metadata(caminho)?;
    println!("  Somente leitura: {}", meta.permissions().readonly());

    // Restaurar permissões normais (Unix)
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let perms = fs::Permissions::from_mode(0o644);
        fs::set_permissions(caminho, perms)?;
        println!("Permissões restauradas para 644");
    }

    fs::remove_file(caminho)?;
    Ok(())
}

fn main() -> std::io::Result<()> {
    gerenciar_permissoes("arquivo_teste.txt")
}

Arquivos temporários com tempfile

Para arquivos temporários seguros:

tempfile = "3"
use tempfile::{tempdir, NamedTempFile};
use std::io::{Write, Read};

fn usar_arquivos_temporarios() -> std::io::Result<()> {
    // Arquivo temporário — deletado ao sair de escopo
    let mut arq_temp = NamedTempFile::new()?;
    writeln!(arq_temp, "Dados temporários")?;

    println!("Arquivo temp: {}", arq_temp.path().display());

    let mut conteudo = String::new();
    arq_temp.reopen()?.read_to_string(&mut conteudo)?;
    println!("Conteúdo: {conteudo}");
    // arq_temp deletado automaticamente aqui

    // Diretório temporário
    let dir_temp = tempdir()?;
    let caminho_arquivo = dir_temp.path().join("dados.txt");

    std::fs::write(&caminho_arquivo, "dados no dir temp")?;
    println!("Dir temp: {}", dir_temp.path().display());
    println!("Arquivo: {}", caminho_arquivo.display());

    // dir_temp e tudo dentro é deletado ao sair de escopo
    Ok(())
}

fn main() -> std::io::Result<()> {
    usar_arquivos_temporarios()
}

Exercícios

Exercício 1

Escreva código que leia um arquivo linha a linha sem carregá-lo inteiro na memória, e explique por que isso importa.

Ver resposta

✓ Resposta:

use std::fs::File;
use std::io::{BufRead, BufReader};

fn main() -> std::io::Result<()> {
    let arquivo = File::open("dados.txt")?;
    for linha in BufReader::new(arquivo).lines() {
        println!("{}", linha?);
    }
    Ok(())
}

O read_to_string carrega tudo de uma vez, o que falha num arquivo de vários gigabytes. O BufReader lê em blocos e entrega uma linha por vez, com uso de memória constante. Note o linha? dentro do laço: cada leitura pode falhar por conta própria — disco com erro, bytes que não são UTF-8 válido.

Exercício 2

Explique a diferença entre Command::new().spawn(), .output() e .status().

Ver resposta

✓ Resposta: spawn() inicia o processo e devolve o controle imediatamente, com um Child para acompanhar depois — é o caminho para rodar algo em paralelo. output() espera terminar e devolve saída padrão, saída de erro e código de retorno capturados. status() espera terminar mas não captura as saídas, deixando-as ir para o terminal, e devolve só o código. O erro comum é usar spawn() e esquecer o wait(), deixando processos zumbis acumularem.

Exercício 3

Por que ler variável de ambiente com env::var devolve Result e não String?

Ver resposta

✓ Resposta: Porque a variável pode não existir, e pode conter bytes que não formam UTF-8 válido — os dois casos aparecem no Result. A assinatura obriga a decidir o que fazer, e o idioma mais comum é env::var("PORTA").unwrap_or_else(|_| "8080".into()). Para configuração obrigatória, expect("defina DATABASE_URL") é aceitável: falhar cedo com mensagem clara é melhor que subir a aplicação e quebrar na primeira consulta.

Exercício 4

Explique por que tratar sinais exige cuidado especial e o que é seguro fazer no tratador.

Ver resposta

✓ Resposta: O sinal interrompe o programa em ponto arbitrário — possivelmente no meio de uma alocação ou com um cadeado tomado. Fazer trabalho ali pode causar impasse ou corrupção, e por isso só um conjunto restrito de operações é seguro (async-signal-safe). O padrão é o tratador apenas registrar que o sinal chegou, marcando um AtomicBool, e o laço principal reagir a isso no momento oportuno. É o que a crate signal-hook facilita, e o mecanismo por trás de um encerramento gracioso ao receber SIGTERM.

Exercício 5

Escreva código que crie um arquivo temporário garantindo a remoção mesmo em caso de erro.

Ver resposta

✓ Resposta:

use tempfile::NamedTempFile;
use std::io::Write;

fn main() -> std::io::Result<()> {
    let mut arquivo = NamedTempFile::new()?;
    writeln!(arquivo, "conteúdo temporário")?;
    // ... uso ...
    Ok(())   // removido aqui, mesmo se tivesse havido erro antes
}

O NamedTempFile apaga o arquivo no Drop — RAII outra vez, agora sobre o sistema de arquivos. Criar o arquivo à mão em /tmp com nome fixo é pior por dois motivos: deixa lixo quando o programa falha e abre uma condição de corrida clássica, em que outro processo cria o mesmo caminho antes.

Fontes e leituras recomendadas

Comentários

Mais em Rust

Segurança e Auditoria — Fuzzing, Análise Estática e Hardening de Código Rust
Segurança e Auditoria — Fuzzing, Análise Estática e Hardening de Código Rust

Seguro em memória não quer dizer seguro: Rust não impede vazamento, impasse…

Parsing e Processamento de Texto — nom, pest e Expressões Regulares
Parsing e Processamento de Texto — nom, pest e Expressões Regulares

Split resolve formato com delimitador, regex resolve padrão que varia, e…

Construindo uma API REST com Axum
Construindo uma API REST com Axum

Uma API REST em Rust começa por entender extratores: tipos que sabem se…