Automatizar a execucao de tarefas em intervalos regulares e uma necessidade fundamental em qualquer ambiente de producao. Desde backups periodicos ate limpeza de cache, um agendador de tarefas confiavel e indispensavel. Neste projeto, vamos construir um agendador de tarefas inspirado no cron, capaz de interpretar expressoes de agendamento, executar comandos de forma assincrona e capturar toda a saida.
Voce aprendera a trabalhar com parsing de expressoes temporais, execucao assincrona com tokio, gerenciamento de processos filhos e controle preciso de tempo.
O Que Vamos Construir
- Parser de expressoes de agendamento simplificadas (estilo cron)
- Execucao assincrona de comandos com
tokio - Captura de stdout e stderr dos processos
- Arquivo de configuracao em TOML para definir tarefas
- Log de execucoes com timestamp e status
- Suporte a multiplas tarefas simultaneas
Estrutura do Projeto
task-scheduler/
├── Cargo.toml
└── src/
├── main.rs
├── parser.rs
├── executor.rs
└── scheduler.rs
Configurando o Projeto
cargo new task-scheduler
cd task-scheduler
Edite o Cargo.toml:
[package]
name = "task-scheduler"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
chrono = "0.4"
toml = "0.8"
serde = { version = "1.0", features = ["derive"] }
colored = "2.1"
Passo 1: Parser de Expressoes de Agendamento
Vamos implementar um parser simplificado que suporta expressoes como */5 (a cada 5 minutos), 30 (no minuto 30) e * (todo minuto). Crie src/parser.rs:
use chrono::{Local, Timelike};
/// Representa uma expressao de agendamento com campos para minuto e hora.
#[derive(Debug, Clone)]
pub struct Expressao {
pub minuto: CampoTempo,
pub hora: CampoTempo,
}
/// Cada campo pode ser: qualquer valor, um valor fixo ou um intervalo.
#[derive(Debug, Clone)]
pub enum CampoTempo {
/// Corresponde a qualquer valor (*)
Qualquer,
/// Corresponde a um valor exato (ex: 30)
Exato(u32),
/// Corresponde a um intervalo (ex: */5 = a cada 5 unidades)
Intervalo(u32),
}
impl CampoTempo {
/// Parseia uma string em um CampoTempo.
pub fn parsear(s: &str) -> Result<Self, String> {
let s = s.trim();
if s == "*" {
return Ok(CampoTempo::Qualquer);
}
if let Some(intervalo) = s.strip_prefix("*/") {
let valor: u32 = intervalo
.parse()
.map_err(|_| format!("Intervalo invalido: {}", s))?;
if valor == 0 {
return Err("Intervalo nao pode ser zero".to_string());
}
return Ok(CampoTempo::Intervalo(valor));
}
let valor: u32 = s
.parse()
.map_err(|_| format!("Valor invalido: {}", s))?;
Ok(CampoTempo::Exato(valor))
}
/// Verifica se o valor atual corresponde a esta expressao.
pub fn corresponde(&self, valor: u32) -> bool {
match self {
CampoTempo::Qualquer => true,
CampoTempo::Exato(v) => *v == valor,
CampoTempo::Intervalo(intervalo) => valor % intervalo == 0,
}
}
}
impl Expressao {
/// Parseia uma expressao no formato "minuto hora" (ex: "*/5 *").
pub fn parsear(expressao: &str) -> Result<Self, String> {
let partes: Vec<&str> = expressao.split_whitespace().collect();
if partes.len() != 2 {
return Err(format!(
"Expressao deve ter 2 campos (minuto hora), recebeu: '{}'",
expressao
));
}
Ok(Expressao {
minuto: CampoTempo::parsear(partes[0])?,
hora: CampoTempo::parsear(partes[1])?,
})
}
/// Verifica se a expressao corresponde ao horario atual.
pub fn deve_executar_agora(&self) -> bool {
let agora = Local::now();
self.minuto.corresponde(agora.minute())
&& self.hora.corresponde(agora.hour())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn teste_parsear_qualquer() {
let campo = CampoTempo::parsear("*").unwrap();
assert!(campo.corresponde(0));
assert!(campo.corresponde(30));
}
#[test]
fn teste_parsear_intervalo() {
let campo = CampoTempo::parsear("*/5").unwrap();
assert!(campo.corresponde(0));
assert!(campo.corresponde(15));
assert!(!campo.corresponde(3));
}
#[test]
fn teste_parsear_exato() {
let campo = CampoTempo::parsear("30").unwrap();
assert!(campo.corresponde(30));
assert!(!campo.corresponde(15));
}
#[test]
fn teste_expressao_completa() {
let expr = Expressao::parsear("*/10 *").unwrap();
assert!(expr.minuto.corresponde(0));
assert!(expr.minuto.corresponde(20));
}
}
O parser converte expressoes de texto em estruturas que podem ser avaliadas contra o horario atual. Isso permite verificar a cada minuto se uma tarefa deve ser executada.
Passo 2: Executor de Comandos
O executor e responsavel por rodar os comandos e capturar sua saida. Crie src/executor.rs:
use chrono::Local;
use colored::*;
use std::process::Stdio;
use tokio::process::Command;
/// Resultado da execucao de um comando.
#[derive(Debug)]
pub struct ResultadoExecucao {
pub comando: String,
pub codigo_saida: Option<i32>,
pub stdout: String,
pub stderr: String,
pub duracao_ms: u128,
pub sucesso: bool,
}
/// Executa um comando de forma assincrona e captura toda a saida.
pub async fn executar_comando(comando: &str) -> ResultadoExecucao {
let inicio = std::time::Instant::now();
let agora = Local::now().format("%Y-%m-%d %H:%M:%S");
println!(
"{} {} Executando: {}",
agora.to_string().dimmed(),
"[EXEC]".cyan(),
comando.white().bold()
);
let resultado = Command::new("sh")
.arg("-c")
.arg(comando)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await;
let duracao = inicio.elapsed().as_millis();
match resultado {
Ok(saida) => {
let stdout = String::from_utf8_lossy(&saida.stdout).to_string();
let stderr = String::from_utf8_lossy(&saida.stderr).to_string();
let codigo = saida.status.code();
let sucesso = saida.status.success();
if sucesso {
println!(
"{} {} Concluido em {}ms (codigo: {})",
Local::now().format("%Y-%m-%d %H:%M:%S").to_string().dimmed(),
"[ OK]".green(),
duracao,
codigo.unwrap_or(-1)
);
} else {
println!(
"{} {} Falhou em {}ms (codigo: {})",
Local::now().format("%Y-%m-%d %H:%M:%S").to_string().dimmed(),
"[ERRO]".red().bold(),
duracao,
codigo.unwrap_or(-1)
);
}
if !stdout.is_empty() {
for linha in stdout.lines() {
println!(" {} {}", "|".dimmed(), linha);
}
}
if !stderr.is_empty() {
for linha in stderr.lines() {
println!(" {} {}", "|".red(), linha);
}
}
ResultadoExecucao {
comando: comando.to_string(),
codigo_saida: codigo,
stdout,
stderr,
duracao_ms: duracao,
sucesso,
}
}
Err(e) => {
println!(
"{} {} Erro ao executar: {}",
Local::now().format("%Y-%m-%d %H:%M:%S").to_string().dimmed(),
"[ERRO]".red().bold(),
e
);
ResultadoExecucao {
comando: comando.to_string(),
codigo_saida: None,
stdout: String::new(),
stderr: e.to_string(),
duracao_ms: duracao,
sucesso: false,
}
}
}
}
O executor usa tokio::process::Command para rodar comandos sem bloquear a thread principal. Isso permite que multiplas tarefas sejam executadas simultaneamente sem interferir no loop principal do agendador.
Passo 3: Agendador Principal
O scheduler coordena a verificacao de horarios e o disparo das tarefas. Crie src/scheduler.rs:
use crate::executor;
use crate::parser::Expressao;
use serde::Deserialize;
use std::path::Path;
/// Configuracao de uma tarefa individual.
#[derive(Debug, Deserialize, Clone)]
pub struct ConfigTarefa {
pub nome: String,
pub agenda: String,
pub comando: String,
}
/// Configuracao completa do agendador.
#[derive(Debug, Deserialize)]
pub struct Configuracao {
#[serde(rename = "tarefa")]
pub tarefas: Vec<ConfigTarefa>,
}
/// Representa uma tarefa pronta para execucao com sua expressao parseada.
#[derive(Debug, Clone)]
pub struct Tarefa {
pub nome: String,
pub expressao: Expressao,
pub comando: String,
}
/// Carrega as tarefas de um arquivo TOML de configuracao.
pub fn carregar_configuracao(caminho: &Path) -> Result<Vec<Tarefa>, Box<dyn std::error::Error>> {
let conteudo = std::fs::read_to_string(caminho)?;
let config: Configuracao = toml::from_str(&conteudo)?;
let mut tarefas = Vec::new();
for cfg in config.tarefas {
let expressao = Expressao::parsear(&cfg.agenda)
.map_err(|e| format!("Erro na tarefa '{}': {}", cfg.nome, e))?;
tarefas.push(Tarefa {
nome: cfg.nome,
expressao,
comando: cfg.comando,
});
}
Ok(tarefas)
}
/// Verifica quais tarefas devem ser executadas e as dispara.
pub async fn verificar_e_executar(tarefas: &[Tarefa]) {
let mut handles = Vec::new();
for tarefa in tarefas {
if tarefa.expressao.deve_executar_agora() {
let comando = tarefa.comando.clone();
let nome = tarefa.nome.clone();
let handle = tokio::spawn(async move {
println!("\n--- Tarefa: {} ---", nome);
let resultado = executor::executar_comando(&comando).await;
(nome, resultado)
});
handles.push(handle);
}
}
for handle in handles {
if let Ok((nome, resultado)) = handle.await {
if !resultado.sucesso {
eprintln!("Tarefa '{}' falhou com codigo {:?}", nome, resultado.codigo_saida);
}
}
}
}
O agendador le as tarefas de um arquivo TOML, parseia suas expressoes e verifica a cada minuto quais devem ser disparadas. Tarefas que coincidem sao executadas de forma concorrente usando tokio::spawn.
Passo 4: Juntando Tudo no main.rs
Crie src/main.rs:
mod executor;
mod parser;
mod scheduler;
use chrono::Local;
use colored::*;
use std::path::PathBuf;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
let caminho_config = if args.len() > 1 {
PathBuf::from(&args[1])
} else {
PathBuf::from("tarefas.toml")
};
if !caminho_config.exists() {
eprintln!("Arquivo de configuracao nao encontrado: {}", caminho_config.display());
eprintln!("\nCrie um arquivo '{}' com o seguinte formato:", caminho_config.display());
eprintln!(
r#"
[[tarefa]]
nome = "Verificar disco"
agenda = "*/5 *"
comando = "df -h | head -5"
[[tarefa]]
nome = "Data atual"
agenda = "* *"
comando = "date"
"#
);
std::process::exit(1);
}
println!("{}", "=== Agendador de Tarefas Rust ===".green().bold());
println!(
"Inicio: {}\n",
Local::now().format("%Y-%m-%d %H:%M:%S")
);
let tarefas = scheduler::carregar_configuracao(&caminho_config)?;
println!("Tarefas carregadas:");
for (i, tarefa) in tarefas.iter().enumerate() {
println!(
" {}. {} - agenda: '{}' -> {}",
i + 1,
tarefa.nome.cyan(),
format!("{:?}", tarefa.expressao).dimmed(),
tarefa.comando
);
}
println!(
"\nAgendador ativo. Verificando a cada 60 segundos...\n{}",
"Pressione Ctrl+C para encerrar.".dimmed()
);
// Executar verificacao imediata
scheduler::verificar_e_executar(&tarefas).await;
// Loop principal: verificar a cada 60 segundos
let mut intervalo = tokio::time::interval(tokio::time::Duration::from_secs(60));
loop {
intervalo.tick().await;
scheduler::verificar_e_executar(&tarefas).await;
}
}
O ponto de entrada carrega o arquivo de configuracao, exibe as tarefas registradas e entra em um loop infinito que verifica a cada 60 segundos se alguma tarefa deve ser executada.
Como Executar
Primeiro, crie o arquivo de configuracao tarefas.toml na raiz do projeto:
[[tarefa]]
nome = "Data e hora"
agenda = "* *"
comando = "date '+%Y-%m-%d %H:%M:%S'"
[[tarefa]]
nome = "Uso de memoria"
agenda = "*/2 *"
comando = "free -h | head -3"
[[tarefa]]
nome = "Espaco em disco"
agenda = "*/5 *"
comando = "df -h / | tail -1"
[[tarefa]]
nome = "Processos ativos"
agenda = "0 *"
comando = "ps aux | wc -l"
Agora compile e execute:
# Compilar
cargo build --release
# Executar com arquivo padrao (tarefas.toml)
cargo run
# Executar com arquivo especifico
cargo run -- meu_agendamento.toml
Saida esperada:
=== Agendador de Tarefas Rust ===
Inicio: 2026-01-15 10:30:00
Tarefas carregadas:
1. Data e hora - agenda: '* *' -> date '+%Y-%m-%d %H:%M:%S'
2. Uso de memoria - agenda: '*/2 *' -> free -h | head -3
3. Espaco em disco - agenda: '*/5 *' -> df -h / | tail -1
4. Processos ativos - agenda: '0 *' -> ps aux | wc -l
Agendador ativo. Verificando a cada 60 segundos...
Pressione Ctrl+C para encerrar.
--- Tarefa: Data e hora ---
2026-01-15 10:30:00 [EXEC] Executando: date '+%Y-%m-%d %H:%M:%S'
2026-01-15 10:30:00 [ OK] Concluido em 5ms (codigo: 0)
| 2026-01-15 10:30:00
--- Tarefa: Uso de memoria ---
2026-01-15 10:30:00 [EXEC] Executando: free -h | head -3
2026-01-15 10:30:00 [ OK] Concluido em 8ms (codigo: 0)
| total used free
| Mem: 16Gi 4.2Gi 11Gi
Desafios para Expandir
- Expressoes cron completas: Expanda o parser para suportar os 5 campos do cron real (minuto, hora, dia do mes, mes, dia da semana) incluindo listas e faixas como
1,15e9-17. - Persistencia de historico: Salve o historico de execucoes em um arquivo SQLite usando a crate
rusqlite, permitindo consultar execucoes passadas e estatisticas. - Notificacoes de falha: Implemente envio de alertas por webhook ou email quando uma tarefa falhar, utilizando a crate
reqwestpara chamadas HTTP. - Timeout por tarefa: Adicione um campo
timeoutna configuracao que cancele tarefas que excedam o tempo limite usandotokio::time::timeout. - Interface web: Crie uma API REST simples com
axumpara visualizar tarefas agendadas, historico de execucoes e disparar execucoes manuais.