File Watcher em Zig — Tutorial Passo a Passo
Neste tutorial, vamos construir um monitor de sistema de arquivos que detecta alterações (criação, modificação, exclusão) em diretórios monitorados e executa ações configuráveis. Em Linux, usamos a API inotify; como fallback, usamos polling com verificação de timestamps.
O Que Vamos Construir
Nosso file watcher vai:
- Monitorar um ou mais diretórios por alterações
- Detectar criação, modificação e exclusão de arquivos
- Filtrar por extensão de arquivo (.zig, .txt, etc.)
- Executar comandos customizados quando mudanças são detectadas
- Suportar debounce para evitar execuções duplicadas
- Usar inotify no Linux para eficiência
Pré-requisitos
- Zig 0.13+ instalado (guia de instalação)
- Familiaridade com I/O de arquivos em Zig
- Linux (para inotify) ou qualquer OS (modo polling)
Passo 1: Estrutura do Projeto
mkdir file-watcher
cd file-watcher
zig init
Passo 2: Tipos de Evento
const std = @import("std");
const fs = std.fs;
const mem = std.mem;
const posix = std.posix;
const Allocator = std.mem.Allocator;
/// Tipo de evento no sistema de arquivos.
const TipoEvento = enum {
criado,
modificado,
removido,
renomeado,
pub fn nome(self: TipoEvento) []const u8 {
return switch (self) {
.criado => "CRIADO",
.modificado => "MODIFICADO",
.removido => "REMOVIDO",
.renomeado => "RENOMEADO",
};
}
pub fn cor(self: TipoEvento) []const u8 {
return switch (self) {
.criado => "\x1b[32m",
.modificado => "\x1b[33m",
.removido => "\x1b[31m",
.renomeado => "\x1b[36m",
};
}
};
/// Um evento detectado no sistema de arquivos.
const EventoArquivo = struct {
tipo: TipoEvento,
caminho: [256]u8,
caminho_len: usize,
timestamp: i64,
pub fn caminhoStr(self: *const EventoArquivo) []const u8 {
return self.caminho[0..self.caminho_len];
}
};
/// Configuração do watcher.
const ConfigWatcher = struct {
extensoes: [8][]const u8 = undefined,
num_extensoes: usize = 0,
debounce_ms: u64 = 500,
recursivo: bool = false,
comando: ?[]const u8 = null,
pub fn aceitaExtensao(self: *const ConfigWatcher, nome_arquivo: []const u8) bool {
if (self.num_extensoes == 0) return true; // aceita tudo
for (self.extensoes[0..self.num_extensoes]) |ext| {
if (mem.endsWith(u8, nome_arquivo, ext)) return true;
}
return false;
}
};
Passo 3: Watcher com Polling
O modo polling funciona em qualquer sistema operacional, verificando timestamps periodicamente.
/// Estado de um arquivo monitorado.
const EstadoArquivo = struct {
nome: [256]u8,
nome_len: usize,
tamanho: u64,
mtime: i128,
existe: bool,
pub fn nomeStr(self: *const EstadoArquivo) []const u8 {
return self.nome[0..self.nome_len];
}
};
/// Watcher baseado em polling (verificação periódica).
const PollingWatcher = struct {
diretorio: []const u8,
estados: [512]EstadoArquivo,
num_estados: usize,
config: ConfigWatcher,
const Self = @This();
pub fn init(diretorio: []const u8, config: ConfigWatcher) Self {
return .{
.diretorio = diretorio,
.estados = undefined,
.num_estados = 0,
.config = config,
};
}
/// Escaneia o diretório e atualiza o estado interno.
/// Retorna os eventos detectados.
pub fn verificar(self: *Self, eventos: []EventoArquivo) !usize {
var num_eventos: usize = 0;
// Escaneia diretório atual
var dir = fs.cwd().openDir(self.diretorio, .{ .iterate = true }) catch return 0;
defer dir.close();
// Marca todos como possivelmente removidos
var i: usize = 0;
while (i < self.num_estados) : (i += 1) {
self.estados[i].existe = false;
}
var iter = dir.iterate();
while (try iter.next()) |entry| {
if (entry.kind != .file) continue;
if (!self.config.aceitaExtensao(entry.name)) continue;
// Busca estado anterior
const stat = dir.statFile(entry.name) catch continue;
const idx = self.encontrarEstado(entry.name);
if (idx) |existing| {
self.estados[existing].existe = true;
// Verificar modificação
if (stat.mtime != self.estados[existing].mtime or
stat.size != self.estados[existing].tamanho)
{
self.estados[existing].mtime = stat.mtime;
self.estados[existing].tamanho = stat.size;
if (num_eventos < eventos.len) {
var ev = &eventos[num_eventos];
ev.tipo = .modificado;
const len = @min(entry.name.len, ev.caminho.len);
@memcpy(ev.caminho[0..len], entry.name[0..len]);
ev.caminho_len = len;
ev.timestamp = std.time.timestamp();
num_eventos += 1;
}
}
} else {
// Novo arquivo
if (self.num_estados < self.estados.len) {
var estado = &self.estados[self.num_estados];
const nlen = @min(entry.name.len, estado.nome.len);
@memcpy(estado.nome[0..nlen], entry.name[0..nlen]);
estado.nome_len = nlen;
estado.tamanho = stat.size;
estado.mtime = stat.mtime;
estado.existe = true;
self.num_estados += 1;
if (num_eventos < eventos.len) {
var ev = &eventos[num_eventos];
ev.tipo = .criado;
@memcpy(ev.caminho[0..nlen], entry.name[0..nlen]);
ev.caminho_len = nlen;
ev.timestamp = std.time.timestamp();
num_eventos += 1;
}
}
}
}
// Detectar removidos
i = 0;
while (i < self.num_estados) {
if (!self.estados[i].existe) {
if (num_eventos < eventos.len) {
var ev = &eventos[num_eventos];
ev.tipo = .removido;
const nlen = self.estados[i].nome_len;
@memcpy(ev.caminho[0..nlen], self.estados[i].nome[0..nlen]);
ev.caminho_len = nlen;
ev.timestamp = std.time.timestamp();
num_eventos += 1;
}
// Remover do array (swap com último)
self.estados[i] = self.estados[self.num_estados - 1];
self.num_estados -= 1;
} else {
i += 1;
}
}
return num_eventos;
}
fn encontrarEstado(self: *Self, nome: []const u8) ?usize {
for (self.estados[0..self.num_estados], 0..) |*estado, idx| {
if (mem.eql(u8, estado.nomeStr(), nome)) return idx;
}
return null;
}
};
Passo 4: Loop Principal
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const stdin = std.io.getStdIn().reader();
try stdout.print(
\\
\\ ==========================================
\\ FILE WATCHER - Zig
\\ ==========================================
\\
, .{});
var buf: [256]u8 = undefined;
try stdout.print(" Diretorio para monitorar (. = atual): ", .{});
const dir_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch "." orelse ".";
const diretorio = mem.trim(u8, dir_raw, " \t\r\n");
var config = ConfigWatcher{};
try stdout.print(" Extensoes para filtrar (ex: .zig,.txt ou vazio para todas): ", .{});
const ext_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch "" orelse "";
const ext_str = mem.trim(u8, ext_raw, " \t\r\n");
if (ext_str.len > 0) {
var ext_iter = mem.splitScalar(u8, ext_str, ',');
while (ext_iter.next()) |ext| {
if (config.num_extensoes < 8) {
config.extensoes[config.num_extensoes] = mem.trim(u8, ext, " ");
config.num_extensoes += 1;
}
}
}
try stdout.print(
\\
\\ Monitorando: {s}
\\ Filtros: {d} extensoes
\\ Intervalo: {d}ms
\\
\\ Pressione Ctrl+C para parar...
\\
, .{ diretorio, config.num_extensoes, config.debounce_ms });
var watcher = PollingWatcher.init(diretorio, config);
// Scan inicial (silencioso)
var eventos_iniciais: [64]EventoArquivo = undefined;
_ = try watcher.verificar(&eventos_iniciais);
// Loop de monitoramento
while (true) {
std.time.sleep(config.debounce_ms * std.time.ns_per_ms);
var eventos: [64]EventoArquivo = undefined;
const num = try watcher.verificar(&eventos);
for (eventos[0..num]) |*ev| {
const reset = "\x1b[0m";
try stdout.print(" {s}[{s}]{s} {s}\n", .{
ev.tipo.cor(), ev.tipo.nome(), reset, ev.caminhoStr(),
});
}
}
}
Testes
test "config aceita extensao" {
var config = ConfigWatcher{};
config.extensoes[0] = ".zig";
config.extensoes[1] = ".txt";
config.num_extensoes = 2;
try std.testing.expect(config.aceitaExtensao("main.zig"));
try std.testing.expect(config.aceitaExtensao("readme.txt"));
try std.testing.expect(!config.aceitaExtensao("image.png"));
}
test "config sem filtro aceita tudo" {
const config = ConfigWatcher{};
try std.testing.expect(config.aceitaExtensao("qualquer.xyz"));
}
test "tipo evento nome" {
try std.testing.expectEqualStrings("CRIADO", TipoEvento.criado.nome());
try std.testing.expectEqualStrings("REMOVIDO", TipoEvento.removido.nome());
}
Compilando e Executando
zig build test
zig build run
Conceitos Aprendidos
- Iteração de diretórios com
std.fs - Comparação de timestamps para detecção de mudanças
- Polling como estratégia de monitoramento portável
- Filtragem por extensão de arquivo
- Arrays fixos para evitar alocação dinâmica
- Loop de monitoramento com
std.time.sleep
Próximos Passos
- Combine com o Analisador de Logs para monitoramento de logs em tempo real
- Explore a documentação de filesystem da stdlib
- Construa o próximo projeto: Mini Grep